zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Server framework

Request Pipeline & AdaptersSupported

The router ties everything together. Register a controller instance and the router reads its routes once, then dispatches each request through: select version → match → build Ctx → guards → validate body → invoke handler → serialize. With no version strategy, version selection is absent. Thin adapters connect it to node:http or any Fetch runtime (Hono, edge) with no hard dependency on either.

Creating a router#

import { createRouter } from '@zmdb/web';
import { Controller, Get, Post } from '@zmdb/web';
import type { Ctx } from '@zmdb/web';

@Controller('/users')
class UsersController {
  @Get('/:id')
  get(ctx: Ctx<{ id: string }>) {
    return { id: ctx.params.id };
  }

  @Post()
  create(ctx: Ctx<Record<never, string>, { name: string }>) {
    return { created: ctx.body.name };
  }
}

const router = createRouter({
  guardRegistry: { app: [authenticated] },
});
router.register(new UsersController(), {
  // optional per-handler guards run after app/controller guards
  create: { guards: [mayCreateUser], validateBody: raw => assertCreateUser(raw) },
});

The pipeline#

router.handle(req) returns { status, body, headers }, where body is tagged as text, bytes or stream:

stepbehavior
matchpath versions are ordinary expanded paths; header/media strategies select method + version + segment count, then matchCompiled; unsupported matched versions → 400/406, unknown path → 404
guardsapp → controller → route; first false403. @Public() bypasses inherited guards
validateif the route has validateBody, run it on the raw body; throw → 400, handler not called
invokecall the handler with the typed Ctx
serializeJSON-encode the result → 200; a handler throw carrying issues400, a built-in multipart boundary error keeps 400/413, any other throw → 500. A result from json/text/bytes/stream/file/respond is returned verbatim
await router.handle({ method: 'GET', path: '/users/42', headers: {} });
// { status: 200, body: { kind: 'text', value: '{"id":"42"}' }, ... }

await router.handle({ method: 'POST', path: '/users', headers: {}, rawBody: { nope: 1 } });
// { status: 400, ... }  — validateBody threw; create() never ran
❗ Important

Validation runs before the handler, so an invalid body never reaches your code. Pair validateBody with @zmdb/validator's assert for zero-runtime-parser validation against a schema DTO.

Adapters (no hard deps)#

import { toNodeHandler, toFetchHandler } from '@zmdb/web';
import { createServer } from 'node:http';

// node:http
createServer(toNodeHandler(router)).listen(3000);

// Fetch (Hono, Bun, Deno, edge)
const handler = toFetchHandler(router); // (Request) => Promise<Response>

Both adapters are structurally typed@zmdb/web does not depend on node:http or Hono; you bring the runtime. Both default request bodies to 1 MiB; pass { maxBodyBytes } to raise that bound.

Design notes#