Note
This is one of 202 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.
🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️
Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors.
The Server
provides
a thin veneer around the standard node:http
/ node:https
default server
implementations.
- Declarative & parametric routing (incl. validation and coercion of route
params)
- Uses @thi.ng/router as implementation
- 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 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.
authenticateWith()
: Predicate function based authenticationcacheControl()
: Cache control header injectioncrossOriginOpenerPolicy()
: Policy header injectioncrossOriginResourcePolicy()
: Policy header injectioninjectHeaders()
: Arbitrary header injectionmeasure()
: Request process timing infologRequest()
: Request detail logginglogResponse()
: Response loggingrateLimiter()
: Configurable rate limitingreferrerPolicy()
: Policy header injectionsessionInterceptor()
: User defined in-memory sessions with TTLstrictTransportSecurity()
: Policy header injection
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()),
};
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 }),
]
}
}
}
ALPHA - bleeding edge / work-in-progress
Search or submit any issues for this package
yarn add @thi.ng/server
ESM import:
import * as ser from "@thi.ng/server";
Browser ESM import:
<script type="module" src="https://esm.run/@thi.ng/server"></script>
For Node.js REPL:
const ser = await import("@thi.ng/server");
Package sizes (brotli'd, pre-treeshake): ESM: 5.28 KB
- @thi.ng/api
- @thi.ng/arrays
- @thi.ng/cache
- @thi.ng/checks
- @thi.ng/errors
- @thi.ng/file-io
- @thi.ng/logger
- @thi.ng/mime
- @thi.ng/paths
- @thi.ng/router
- @thi.ng/strings
- @thi.ng/timestamp
- @thi.ng/uuid
Note: @thi.ng/api is in most cases a type-only import (not used at runtime)
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(),
// 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
If this project contributes to an academic publication, please cite it as:
@misc{thing-server,
title = "@thi.ng/server",
author = "Karsten Schmidt",
note = "https://thi.ng/server",
year = 2024
}
© 2024 - 2025 Karsten Schmidt // Apache License 2.0