Skip to content

Latest commit

 

History

History
240 lines (201 loc) · 7.64 KB

tpl.readme.md

File metadata and controls

240 lines (201 loc) · 7.64 KB

About

{{pkg.description}}

The Server provides a thin veneer around the standard node:http / node:https default server implementations.

Main features

  • Declarative & parametric routing (incl. validation and coercion of route params)
  • Multiple HTTP methods per route
    • Built-in HTTP OPTIONS handler for listing available route methods
    • Fallback HTTP HEAD to GET method (if available)
  • Asynchronous route handler processing
    • Composable & customizable interceptor chains
    • Global interceptors for all routes and/or local for individual routes & HTTP methods
  • Automatic parsing of cookies and URL query strings (incl. nested params)
  • In-memory session storage & route interceptor
  • Configurable file serving (ReadableStream-based) with automatic MIME-type detection and support for Etags, as well as Brotli, Gzip and Deflate compression
  • Utilities for parsing form-encoded multipart request bodies

Interceptors

Interceptors are additionally injected route handlers (aka middleware) which are pre/post-processed before/after a route's main handler and can be used for validation, cancellation or other side effects. Each single interceptor can have a pre and/or post phase function. Each route handler can define its own interceptor chains, which will be appended to the globally defined interceptors (applied to all routes). Post-phase interceptors are processed in reverse order. See Interceptor for more details.

Diagram illustrating interceptor processing order

Available interceptors

Custom interceptors

An example interceptor to log request and response headers:

import type { Interceptor } from "@thi.ng/server";

export const log: Interceptor = {
	pre: (ctx) => ctx.logger.debug("request headers", ctx.req.headers),
	post: (ctx) => ctx.logger.debug("response headers", ctx.res.getHeaders()),
};

Using interceptors

An example route definition with route and HTTP-method specific interceptor(s):

import { cacheControl } from "@thi.ng/server";

{
	id: "hello",
	match: "/random",
	handlers: {
		get: {
			fn: (ctx) => ctx.res.writeHead(200).end(String(Math.random())),
			intercept: [
				cacheControl({ noCache: true }),
			]
		}
	}
}

{{meta.status}}

{{repo.supportPackages}}

{{repo.relatedPackages}}

{{meta.blogPosts}}

Installation

{{pkg.install}}

{{pkg.size}}

Dependencies

{{pkg.deps}}

{{repo.examples}}

API

{{pkg.docs}}

Usage example

import * as srv from "@thi.ng/server";

// all route handlers & interceptors receive a request context object
// here we define an extended/customized version
interface AppCtx extends srv.RequestCtx {
	session?: AppSession;
}

// customized version of the default server session type
interface AppSession extends srv.ServerSession {
	user?: string;
	locale?: string;
}

// interceptor for injecting/managing sessions
// by default uses in-memory storage/cache
const session = srv.sessionInterceptor<AppCtx, AppSession>({
	factory: srv.createSession
});

// create server with given config
const app = srv.server<AppCtx>({
	// global interceptors (used for all routes)
	intercept: [
		// log all requests (using server's configured logger)
		srv.logRequest(),
		// block known AI bots
		srv.rejectUserAgents(srv.USER_AGENT_AI_BOTS),
		// lookup/create sessions (using above interceptor)
		session,
		// ensure routes with `auth` flag have a logged-in user
		srv.authenticateWith<AppCtx>((ctx) => !!ctx.session?.user),
	],
	// route definitions (more can be added dynamically later)
	routes: [
		// define a route for serving static assets
		srv.staticFiles({
			// ensure only logged-in users can access
			auth: true,
			// use compression (if client supports it)
			compress: true,
			// route prefix
			prefix: "assets",
			// map to current CWD
			rootDir: ".",
			// strategy for computing etags (optional)
			etag: srv.etagFileHash(),
			// route specific interceptors
			intercept: [srv.cacheControl({ maxAge: 3600 })],
		}),
		// define a dummy login route
		{
			id: "login",
			match: "/login",
			handlers: {
				// each route can specify handlers for various HTTP methods
				post: async (ctx) => {
					const { user, pass } = await srv.parseRequestFormData(ctx.req);
					ctx.logger.info("login details", user, pass);
					if (user === "thi.ng" && pass === "1234") {
						// create new session for security reasons (session fixation)
						const newSession = await session.replaceSession(ctx)!;
						newSession!.user = user;
						ctx.res.writeHead(200).end("logged in as " + user);
					} else {
						ctx.res.unauthorized({}, "login failed");
					}
				},
			},
		},
		// dummy logout route
		{
			id: "logout",
			match: "/logout",
			// use auth flag here to ensure route is only accessible if valid session
			auth: true,
			handlers: {
				get: async (ctx) => {
					// remove session & force expire session cookie
					await session.deleteSession(ctx, ctx.session!.id);
					ctx.res.writeHead(200).end("logged out");
				},
			},
		},
		// parametric route (w/ optional validator)
		{
			id: "hello",
			match: "/hello/?name",
			validate: {
				name: { check: (x) => /^[a-z]+$/i.test(x) },
			},
			handlers: {
				get: async ({ match, res }) => {
					res.writeHead(200, { "content-type": "text/plain" })
					   .end(`hello, ${match.params!.name}!`);
				},
			},
		},
		// another route to demonstrate role/usage of route IDs
		// here we simply attempt to redirect to the above `hello` route
		{
			id: "alias",
			match: "/alias/?name",
			handlers: {
				get: ({ server, match, res }) =>
					server.redirectToRoute(res, {
						id: "hello",
						params: match.params,
					}),
			},
		},
	],
});

await app.start();
// [INFO] server: starting server: http://localhost:8080