@thi.ng/server
Version:
Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors
331 lines (275 loc) • 12.8 kB
Markdown
<!-- This file is generated - DO NOT EDIT! -->
<!-- Please see: https://github.com/thi-ng/umbrella/blob/develop/CONTRIBUTING.md#changes-to-readme-files -->
# 
[](https://www.npmjs.com/package/@thi.ng/server)

[](https://mastodon.thi.ng/@toxi)
> [!NOTE]
> This is one of 205 standalone projects, maintained as part
> of the [.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo
> and anti-framework.
>
> 🚀 Please help me to work full-time on these projects by [sponsoring me on
> GitHub](https://github.com/sponsors/postspectacular). Thank you! ❤️
- [About](#about)
- [Main features](#main-features)
- [Interceptors](#interceptors)
- [Available interceptors](#available-interceptors)
- [Custom interceptors](#custom-interceptors)
- [Using interceptors](#using-interceptors)
- [Static file serving](#static-file-serving)
- [Status](#status)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [API](#api)
- [Usage example](#usage-example)
- [Authors](#authors)
- [License](#license)
## About
Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors.
The [`Server`](https://docs.thi.ng/umbrella/server/classes/Server.html) 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)
- Uses [.ng/router](https://github.com/thi-ng/umbrella/tree/develop/packages/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 [static file serving](#static-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`](https://docs.thi.ng/umbrella/server/interfaces/Interceptor.html)
for more details.

#### Available interceptors
- [`authenticateWith()`](https://docs.thi.ng/umbrella/server/functions/authenticateWith.html): Predicate function based authentication
- [`cacheControl()`](https://docs.thi.ng/umbrella/server/functions/cacheControl.html): Cache control header injection
- [`crossOriginOpenerPolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginOpenerPolicy-1.html): Policy header injection
- [`crossOriginResourcePolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginResourcePolicy-1.html): Policy header injection
- [`injectHeaders()`](https://docs.thi.ng/umbrella/server/functions/injectHeaders.html): Arbitrary header injection
- [`measure()`](https://docs.thi.ng/umbrella/server/functions/measure.html): Request process timing info
- [`logRequest()`](https://docs.thi.ng/umbrella/server/functions/logRequest.html): Request detail logging
- [`logResponse()`](https://docs.thi.ng/umbrella/server/functions/logResponse.html): Response logging
- [`rateLimiter()`](https://docs.thi.ng/umbrella/server/functions/rateLimiter-1.html): Configurable rate limiting
- [`referrerPolicy()`](https://docs.thi.ng/umbrella/server/functions/referrerPolicy-1.html): Policy header injection
- [`rejectUserAgents()`](https://docs.thi.ng/umbrella/server/functions/rejectUserAgents.html): Configurable UA blocking
- [`sessionInterceptor()`](https://docs.thi.ng/umbrella/server/functions/sessionInterceptor-1.html): User defined in-memory sessions with TTL
- [`strictTransportSecurity()`](https://docs.thi.ng/umbrella/server/functions/strictTransportSecurity.html): Policy header injection
#### Custom interceptors
An example interceptor to log request and response headers:
```ts
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):
```ts
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 }),
]
}
}
}
```
### Static file serving
The
[`staticFiles()`](https://docs.thi.ng/umbrella/server/functions/staticFiles.html)
route provider can be used to serve files from a given local root directory.
Multiple such routes can be defined. The handler is highly configurable in terms
of path validation/filtering, global and/or per-file headers, Etag generation,
compression. It also supports its own set of [interceptors](#interceptors).
See
[`StaticOpts`](https://docs.thi.ng/umbrella/server/interfaces/StaticOpts.html)
and example below for more details.
## Status
**ALPHA** - bleeding edge / work-in-progress
[Search or submit any issues for this package](https://github.com/thi-ng/umbrella/issues?q=%5Bserver%5D+in%3Atitle)
## Installation
```bash
yarn add .ng/server
```
ESM import:
```ts
import * as ser from "@thi.ng/server";
```
Browser ESM import:
```html
<script type="module" src="https://esm.run/@thi.ng/server"></script>
```
[JSDelivr documentation](https://www.jsdelivr.com/)
For Node.js REPL:
```js
const ser = await import("@thi.ng/server");
```
Package sizes (brotli'd, pre-treeshake): ESM: 6.27 KB
## Dependencies
- [.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
- [.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
- [.ng/cache](https://github.com/thi-ng/umbrella/tree/develop/packages/cache)
- [.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
- [.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
- [.ng/file-io](https://github.com/thi-ng/umbrella/tree/develop/packages/file-io)
- [.ng/leaky-bucket](https://github.com/thi-ng/umbrella/tree/develop/packages/leaky-bucket)
- [.ng/logger](https://github.com/thi-ng/umbrella/tree/develop/packages/logger)
- [.ng/mime](https://github.com/thi-ng/umbrella/tree/develop/packages/mime)
- [.ng/paths](https://github.com/thi-ng/umbrella/tree/develop/packages/paths)
- [.ng/router](https://github.com/thi-ng/umbrella/tree/develop/packages/router)
- [.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
- [.ng/timestamp](https://github.com/thi-ng/umbrella/tree/develop/packages/timestamp)
- [.ng/uuid](https://github.com/thi-ng/umbrella/tree/develop/packages/uuid)
Note: .ng/api is in _most_ cases a type-only import (not used at runtime)
## API
[Generated API docs](https://docs.thi.ng/umbrella/server/)
### Usage example
```ts tangle:export/readme-hello.ts
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
```
## Authors
- [Karsten Schmidt](https://thi.ng)
If this project contributes to an academic publication, please cite it as:
```bibtex
{thing-server,
title = "@thi.ng/server",
author = "Karsten Schmidt",
note = "https://thi.ng/server",
year = 2024
}
```
## License
© 2024 - 2025 Karsten Schmidt // Apache License 2.0