{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"server","title":"Server primitives","description":"Clank's server layer targets the Fetch standard, not one vendor's runtime. The same app.handle(request) works anywhere with Web Request and Response objects.","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/server","source":"docs/server.md","headings":["Routes","Request context","Middleware","Mount an agent bridge","Node runtime adapter"],"tableOfContents":[{"id":"routes","title":"Routes","level":2},{"id":"request-context","title":"Request context","level":2},{"id":"middleware","title":"Middleware","level":2},{"id":"mount-an-agent-bridge","title":"Mount an agent bridge","level":2},{"id":"node-runtime-adapter","title":"Node runtime adapter","level":2}],"markdown":"# Server primitives\n\nClank's server layer targets the Fetch standard, not one vendor's runtime. The same `app.handle(request)` works anywhere with Web `Request` and `Response` objects.\n\n## Routes\n\n```ts\nconst app = createApp()\n  .get(\"/health\", () => text(\"ok\"))\n  .get(\"/users/:id\", ({ params }) => json({ id: params.id }))\n  .post(\"/messages\", async ({ request }) => {\n    const input = await request.json();\n    return json(await createMessage(input), { status: 201 });\n  });\n\nconst response = await app.handle(request);\n```\n\nLiteral route paths infer their parameter keys. In `.get(\"/users/:id\", handler)`, `params.id` is a string; optional `:tab?` parameters are optional, and `*` exposes the wildcard value.\n\nMethods are `route`, `get`, `post`, `put`, `patch`, and `delete`. Routes use the same parameter, optional-segment, and wildcard matcher as the client router.\n\n## Request context\n\nEvery handler receives:\n\n```ts\ninterface RequestContext {\n  request: Request;\n  url: URL;\n  params: Record<string, string>;\n  state: Record<string, unknown>;\n}\n```\n\nMiddleware can use `state` for request IDs, authenticated users, database handles, timing, or other per-request values.\n\n## Middleware\n\n```ts\napp.use(async (context, next) => {\n  context.state.user = await authenticate(context.request);\n  const response = await next();\n  const headers = new Headers(response.headers);\n  headers.set(\"x-frame-options\", \"DENY\");\n  return new Response(response.body, { status: response.status, headers });\n});\n```\n\nMiddleware executes in registration order and unwinds in reverse order. Calling `next()` twice throws. Unhandled errors become a generic JSON 500 response. Use `createApp({ onError })` for private logging; unexpected exception text is never returned to the client.\n\nBuilt-ins:\n\n```ts\napp.use(logger());\napp.use(cors({ origin: \"https://app.example.com\", credentials: true }));\napp.use(securityHeaders({\n  contentSecurityPolicy: \"default-src 'self'; object-src 'none'; frame-ancestors 'none'\",\n}));\n```\n\nCredentialed CORS rejects wildcard origins at configuration time. Origin arrays and callbacks are available when an exact allowlist is required. CORS controls browser response access; it is not authentication or CSRF protection.\n\nThe response helpers are `json`, `text`, and `html`.\n\n## Mount an agent bridge\n\nThe minimal adapter is a wildcard route:\n\n```ts\napp.route(\"*\", \"/api/agent/*\", ({ request }) => bridge.handle(request, {\n  user: authenticatedUser,\n}));\n```\n\nIf you want the well-known manifest at a separate path, add an explicit route to the same handler.\n\n## Node runtime adapter\n\n`clank/node` provides the dependency-free adapter for Node 22.16+:\n\n```ts\nimport { serve, staticFiles } from \"@clank.run/framework/node\";\n\nconst publicFiles = staticFiles(\"./public\", { prefix: \"/assets\" });\nconst app = createApp()\n  .get(\"/assets/*\", ({ request }) => publicFiles.handle(request))\n  .get(\"/health\", () => text(\"ok\"));\n\nconst server = await serve(app, {\n  hostname: \"127.0.0.1\",\n  port: 3000,\n  maxBodySize: 1024 * 1024,\n});\nawait server.close();\n```\n\n`serve()` translates Node HTTP messages to Fetch objects, streams response bodies for SSE, propagates aborts, preserves multiple cookies, caps bodies and headers, configures timeouts, validates Host headers, and can trust forwarded protocol/client IP only when explicitly enabled. Loopback listeners accept loopback hosts by default; public deployments should set `allowedHosts`.\n\n`staticFiles()` supports GET/HEAD, index files, MIME types, cache headers, dotfile denial, traversal rejection, and post-symlink containment checks.\n\n```ts\nawait serve(app, {\n  hostname: \"0.0.0.0\",\n  allowedHosts: [\"app.example.com\"],\n  trustProxy: true, // only behind a trusted, exclusive reverse proxy\n});\n```\n\nEdge and worker runtimes can usually accept `app.handle` directly as their fetch handler.\n\nFor inferred query/mutation RPC, SQLite, and EventSource live queries, see [Full-stack SSR, SQLite, and live sync](full-stack.md).\n\nFor secure cookies, owned data, and reverse-proxy guidance, see [Authentication](auth.md) and [Security](security.md).\n"}