Runtime & Manifest
The runtime module provides the unified Web handler used by every adapter (Node, Bun, Vercel, Netlify) and the Vite dev server. The manifest module generates a portable description of the app's routes, actions, and islands.
# createWebHandler(routes, actions, options)
Creates a single Web handler function (Request) => Promise<Response> from
scanned routes and actions. This is the entry point for all runtimes:
import { createWebHandler } from "@elurjs/kit/runtime";
import { scanRoutes } from "@elurjs/kit";
import { scanActions } from "@elurjs/kit";
const routes = await scanRoutes("./src/app");
const actions = await scanActions("./src/app");
const handler = createWebHandler(routes, actions, {
staticRoot: "./dist",
lang: "es",
clientEntry: "/_elur/entry-client.js",
renderEndpoint: true,
cacheDir: "./.elur/cache",
defaultRevalidate: 60,
});
// Use with any Web-standard server
const response = await handler(request);# WebHandlerOptions
| Field | Type | Description |
|---|---|---|
staticRoot |
string |
Static file root (absolute path, usually dist/) |
noCache |
boolean? |
Bypass ISR cache (dev mode) |
cacheDir |
string? |
ISR cache directory (absolute) |
defaultRevalidate |
number? |
Default ISR revalidate seconds |
importer |
(path) => Promise<unknown>? |
Custom module loader for bundled adapters |
lang |
string? |
HTML lang attribute |
clientEntry |
string? |
Client entry path |
renderEndpoint |
boolean? |
Whether /__elur-js/render exists |
securityHeaders |
SecurityHeadersConfig | false? |
Security response headers |
logLevel |
LogLevel? |
Minimum level for the per-request structured logger |
cacheAdapter |
CacheAdapter? |
Pluggable ISR cache — default: filesystem adapter at cacheDir |
redirects |
RedirectRule[]? |
Redirect rules evaluated before routing |
rewrites |
RewriteRule[]? |
Transparent path rewrites |
routeHeaders |
RouteHeadersRule[]? |
Extra response headers per path pattern |
streaming |
boolean? |
Opt-in streaming SSR for loading boundaries (experimental) |
capabilities |
AdapterCapabilities? |
Host capabilities gating streaming (default DEFAULT_CAPABILITIES) |
middleware |
LoadedMiddleware? |
User middleware (load with loadMiddleware) |
router |
{ enabled?, entry? }? |
SPA router flags affecting SSR output |
js |
"modern" | "legacy"? |
Client JS emission mode |
Every response carries X-Request-ID (correlation) and Server-Timing
(per-phase metrics: action, render-endpoint, api, ssr) headers.
# WebHandlerRouteTable
interface WebHandlerRouteTable {
pages: PageRoute[];
api: ApiRoute[];
error404?: PageRoute;
error500?: PageRoute;
}# RequestContext
Per-request state container with cookies, locals, params, and response accumulators:
import { RequestContext } from "@elurjs/kit/runtime";
const ctx = new RequestContext({
request,
config: resolvedConfig,
routes,
actions,
publicActions,
});
ctx.pathname; // URL pathname
ctx.method; // HTTP method (uppercased)
ctx.wantsJson; // true if Accept: application/json
ctx.searchParams; // URLSearchParams of the query string
ctx.renderConfig; // { lang, clientEntry, renderEndpoint } for renderPage
ctx.cookies; // read-only cookie jar
ctx.locals; // per-request data from middleware
ctx.params; // route params
ctx.response; // { status, headers, cookies }
ctx.requestId; // unique ID for logging
ctx.signal; // AbortSignal from host disconnect# RequestContextOptions
| Field | Type | Description |
|---|---|---|
request |
Request |
The Web Request |
config |
ResolvedElurConfig |
Resolved config |
routes |
RouteTable |
Scanned routes |
actions |
ActionRegistry |
Action registry |
publicActions |
Record<string, string[]> |
Public action names |
importer |
(path) => unknown | Promise<unknown>? |
Module loader |
renderEndpoint |
boolean? |
Whether render endpoint exists (default true) |
noCache |
boolean? |
Bypass ISR cache (default false) |
cacheDir |
string? |
ISR cache directory |
defaultRevalidate |
number? |
Default ISR revalidate seconds |
params |
Record<string, string | string[] | undefined>? |
Initial route params |
locals |
Record<string, unknown>? |
Initial locals |
signal |
AbortSignal? |
Abort signal |
requestId |
string? |
Custom request ID |
platform |
unknown? |
Platform-specific context |
route |
PageRoute | ApiRoute? |
Pre-matched route |
# RouteTable
interface RouteTable {
pages: PageRoute[];
api: ApiRoute[];
error404?: PageRoute;
error500?: PageRoute;
}# Response helpers
import {
htmlResponse,
jsonResponse,
textResponse,
notFound,
methodNotAllowed,
serverError,
} from "@elurjs/kit/runtime";
htmlResponse("<h1>Hi</h1>", 200);
jsonResponse({ ok: true }, 200);
textResponse("Not found", 404);
notFound(); // 404 "Not Found"
methodNotAllowed("PUT"); // 405
serverError("oops"); // 500# guessContentType(filePath)
Infers Content-Type from file extension:
import { guessContentType } from "@elurjs/kit/runtime";
guessContentType("style.css"); // "text/css; charset=utf-8"# Static file serving
import { serveStaticFile, resolveStaticFile } from "@elurjs/kit/runtime";
// High-level: returns a Response (or null when the path doesn't resolve).
// Pass the incoming Request to enable HEAD, Range and If-* handling.
const response = await serveStaticFile("./dist", "/images/hero.webp", request);
// Low-level: returns the resolved absolute path or null
const filePath = await resolveStaticFile("./dist", "/images/hero.webp");Static serving supports conditional and range requests out of the box:
ETag+If-None-Match→304 Not ModifiedIf-Modified-Since→304Range(single byte range) +If-Range→206/416HEADrequests served without a body- Content-hashed asset URLs get immutable
Cache-Control ..traversal and symlink escapes resolve tonull(→ 404)
There is no built-in compression (gzip/brotli) — put a reverse proxy in
front of elur-kit start if you need compressed responses.
# incomingMessageToRequest(req, body?) / sendWebResponse(res, response)
Convert between Node.js and Web primitives:
import { incomingMessageToRequest, sendWebResponse } from "@elurjs/kit/runtime";
import { createServer } from "node:http";
const server = createServer(async (req, res) => {
const request = incomingMessageToRequest(req);
const response = await handler(request);
await sendWebResponse(res, response);
});sendWebResponse forwards streamed chunks as they are produced (with
backpressure) and cancels the upstream stream when the socket closes —
required for real streaming SSR on Node.
# Structured logging
The handler creates a StructuredLogger per request instead of bare
console.* calls — JSON in production, readable [LEVEL] text in dev,
with structured fields (path, method, route, error, stack),
sensitive-header redaction, and a request ID from the incoming
X-Request-ID header or generated per request:
import { StructuredLogger, createRequestLogger } from "@elurjs/kit/runtime";
// createRequestLogger(request?, minLevel?) — reuses the request's
// X-Request-ID header when present
const logger = createRequestLogger(request, "info"); // "debug" | "info" | "warn" | "error"
logger.info("request handled", { path: "/blog", status: 200 });Configure globally via defineConfig({ logger: { level } }) or the
--verbose/--quiet CLI flags.
# Adapter capabilities
Hosts declare what they support; createWebHandler uses it to gate
streaming, and the CLI adapter command fails fast on incompatible
combinations:
import {
validateCapabilities,
DEFAULT_CAPABILITIES, // Node/Bun — full capabilities
SERVERLESS_CAPABILITIES, // Vercel/Netlify — ephemeral filesystem
EDGE_CAPABILITIES, // Edge — readonly filesystem
createCapabilities,
supportsStreaming,
supportsPersistentStorage,
supportsWritableFilesystem,
} from "@elurjs/kit/runtime";See Deployment — Adapter capabilities for the full capability contract.
# Security headers
import {
buildSecurityHeaders,
applySecurityHeaders,
DEFAULT_SECURITY_HEADERS,
} from "@elurjs/kit/runtime";
// Build headers from config — buildSecurityHeaders(config, isHttps, nonce?)
const headers = buildSecurityHeaders(
config.security.headers,
request.url.startsWith("https"),
);
// Apply to a response
applySecurityHeaders(response, headers);Defaults (DEFAULT_SECURITY_HEADERS): X-Content-Type-Options: nosniff,
Referrer-Policy: strict-origin-when-cross-origin, and
X-Frame-Options: SAMEORIGIN. Behavior details:
- HSTS is only emitted for HTTPS requests unless
hstsis an explicit string value. - CSP — when
contentSecurityPolicyis set it replacesX-Frame-Options; thenoncetoken is substituted per request with'nonce-<value>'. - Never overwrites — a header already set on the response by
middleware, a route handler, or
routes.headersis left untouched. security.headers: falsedisables the whole block.
# App manifest
# createAppManifest(config)
Scans routes, actions, and islands in parallel and returns a portable manifest:
import { createAppManifest } from "@elurjs/kit";
const manifest = await createAppManifest(resolvedConfig);
// manifest.routes, manifest.actions, manifest.islands, manifest.base, manifest.output# AppManifest
interface AppManifest {
version: 1;
root: string;
routes: ScannedRoutes;
actions: ActionRegistry;
islands: IslandModule[];
base: string;
output: "static" | "server" | "hybrid";
}# writeAppManifest(manifest, path)
Writes the manifest as JSON (with portable paths) to disk:
import { writeAppManifest } from "@elurjs/kit";
await writeAppManifest(manifest, "./dist/.elur/manifest.json");# writeRouteTypes(manifest, path)
Generates TypeScript route types for type-safe navigation:
import { writeRouteTypes } from "@elurjs/kit";
await writeRouteTypes(manifest, "./src/types/elur-routes.d.ts");
// Generates:
// export type ElurRoutePath = "/" | "/blog" | "/blog/:slug" | ...;
// export type ElurActionName = "submitContact" | "createPost" | ...;
// export interface ElurRouteParams { [name: string]: string | string[] | undefined }# validateManifestRoutes(routes)
Throws on duplicate or invalid route patterns:
import { validateManifestRoutes } from "@elurjs/kit";
validateManifestRoutes(routes); // throws if conflicts found# assertClientImportAllowed(id, importer?)
Guards against importing server-only modules in client bundles:
import { assertClientImportAllowed } from "@elurjs/kit";
assertClientImportAllowed("some-module", importer);