Docs/Server/Unified Web Handler
GitHub

Unified Web Handler

Since v2.0.2, every runtime path — dev, preview, start, and all four adapters (Node, Bun, Vercel, Netlify) — funnels through a single function: createWebHandler(). This eliminates duplicated request-handling logic and guarantees identical behavior across runtimes.

# createWebHandler()

import { createWebHandler } from "@deijose/nix-js-kit/runtime";

const handler = createWebHandler(routes, actions, {
  staticRoot: "./dist",
  lang: "en",
  clientEntry: "/_nix-js/entry-client.js",
  renderEndpoint: true,
  securityHeaders: true,
  cacheDir: "./.nix-js/cache",
  defaultRevalidate: 60,
});

// handler is a pure Web function: Request → Response
const response = await handler(request);

# Responsibilities (in order)

  1. Server actionsPOST /__nix-js/actions (CSRF-verified, body-limited).
  2. SPA render endpointGET /__nix-js/render?page=<path> (returns body + head for SPA navigation).
  3. API routesroute.ts files.
  4. Static files — served from staticRoot with ETag, Range, HEAD, and immutable caching for hashed assets.
  5. Dynamic SSR — unmatched paths are rendered on demand.
  6. Error pages — custom 404.page.ts and 500.page.ts.

# WebHandlerOptions

Option Type Default Description
staticRoot string Absolute path to the build output directory
noCache boolean false Bypass ISR cache (dev mode)
cacheDir string ISR cache directory
defaultRevalidate number 60 Default ISR TTL in seconds
importer (path) => Promise<unknown> Module loader for adapter-bundled entries
lang string "es" HTML lang attribute
clientEntry string Client entry path
renderEndpoint boolean true Whether the render endpoint exists
securityHeaders SecurityHeadersConfig | false true Security headers config; false disables
ℹ Note

The handler is pure — it does not import Node HTTP types. It works in Bun, Deno, Cloudflare Workers, Vercel Edge, and any Web-standard runtime.

# RequestContext

RequestContext carries per-request state across middleware, loaders, and actions. It's the single context type used by the unified handler.

import type { RequestContext } from "@deijose/nix-js-kit/runtime";

# Fields

Field Type Description
params RouteParams Dynamic segment values ({ slug: "hello" })
locals Record<string, unknown> Per-request mutable state, isolated per request
cookies CookieJar Read request cookies (get, getAll, has)
signal AbortSignal Aborts when the client disconnects
requestId string Auto-generated UUID for logging/tracing
platform { runtime, host } Detected runtime (node, bun, edge)
route PageRoute The matched route
response ResponseState Mutable response state (headers + Set-Cookie)

# CookieJar (read)

interface CookieJar {
  get(name: string): string | undefined;
  getAll(): Record<string, string>;
  has(name: string): boolean;
}

# ResponseCookieJar (write)

interface ResponseCookieJar {
  set(name: string, value: string, options?: CookieOptions): void;
  clear(name: string, options?: CookieOptions): void;
  getAll(): string[];
}

# CookieOptions

Option Type Description
httpOnly boolean HttpOnly flag
secure boolean Secure flag
sameSite "strict" | "lax" | "none" SameSite policy
maxAge number Max age in seconds
expires Date Expiration date
path string Cookie path
domain string Cookie domain

# applyToResponse()

Merges accumulated headers, Set-Cookie values, and status into the final Response:

const ctx = new RequestContext(request, { /* options */ });
ctx.response.headers.set("X-Custom", "value");
ctx.response.cookies.set("session", "abc123", { httpOnly: true });

// Later, when building the response:
const response = ctx.applyToResponse(new Response(html, { headers: { "Content-Type": "text/html" } }));
// response now includes X-Custom and Set-Cookie: session=abc123; HttpOnly

# Using the handler in a custom server

import { createServer } from "node:http";
import { scanRoutes, scanActions, createWebHandler, incomingMessageToRequest } from "@deijose/nix-js-kit/runtime";

const routes = await scanRoutes("src/app");
const actions = await scanActions("src/app");
const handler = createWebHandler(routes, actions, {
  staticRoot: "./dist",
  lang: "en",
  clientEntry: "/_nix-js/entry-client.js",
});

createServer(async (req, res) => {
  const request = incomingMessageToRequest(req);
  const response = await handler(request);
  res.writeHead(response.status, Object.fromEntries(response.headers));
  res.end(await response.text());
}).listen(3000);

# Why a unified handler?

Before v2.0.2, the CLI (dev/preview), start, and each adapter had their own request-handling logic. This meant:

  • Bugs fixed in one path weren't fixed in others.
  • Behavior could differ between dev and production.
  • Security headers, error sanitization, and cache policy had to be reimplemented per runtime.

The unified handler solves all three: one function, one behavior, every runtime.

# Subpath export

import {
  createWebHandler,
  RequestContext,
  serveStaticFile,
  resolveStaticFile,
  incomingMessageToRequest,
  guessContentType,
  htmlResponse,
  jsonResponse,
  textResponse,
  notFound,
  methodNotAllowed,
  serverError,
} from "@deijose/nix-js-kit/runtime";
✎ Edit this page on GitHub