Skip to content

Search docs

Jump between documentation pages.

Browse docs

API reference: Runtime adapters

The runtime adapters wrap the same app.fetch(Request) dispatch for each platform. They are available only as subpaths (never from the root barrel), so runtime-specific code such as node:http never leaks into an edge or Worker bundle. For guide-level setup per platform, see the adapters overview. For the module map, see the API reference overview.

One app, every runtime
web-standard coreapp.fetch(request)Request -> Response
serversserve(app)/node · /bun · /deno
edge & serverlesstoFetchHandler(app)/cloudflare · /vercel · /fastly
aws · azuretoLambdaHandler · toAzureHandler/lambda (APIGW v1 + v2) · /azure (v4)
same behaviorResponseidentical headers, errors, and docs everywhere
Adapters translate each platform's entrypoint into the same web-standard dispatch, so behavior stays identical across runtimes.

@daloyjs/core/node

ts
serve(app: App, opts?: NodeServerOptions): NodeServerHandle;

interface NodeServerOptions {
  port?:                 number;   // default: 3000
  hostname?:             string;   // default: "0.0.0.0"
  connectionTimeoutMs?:  number;   // default: 30_000
  shutdownTimeoutMs?:    number;   // default: 10_000
  handleSignals?:        boolean;  // default: true (SIGINT/SIGTERM)
  maxHeaderBytes?:       number;   // default: 16 KiB
  trustProxy?:           boolean;  // honor x-forwarded-proto/host (only behind a trusted LB); proto only as http/https
  maxConnections?:       number;   // cap concurrent sockets (admission control); default: unset (unbounded)
  bufferedBodyMaxBytes?: number;   // default: 256 KiB (pre-buffer threshold for POST hot path)
}
interface NodeServerHandle { server: Server; port: number; close(): Promise<void> }

// Request-target hardening: raw %2e%2e dot segments are resolved and \ is
// folded to / before routing, and a Host (or trusted X-Forwarded-Host) that
// contains \ / ? # @ % or whitespace is refused with 400. When the client
// disconnects mid-response, a streamed body (SSE, NDJSON) is cancelled.

Pass port: 0 when a test needs an ephemeral port. Because Node binds asynchronously, wait for the server's listening event before reading handle.port. It then reports the OS-assigned port instead of 0.

ts
import { once } from "node:events";

const handle = serve(app, { port: 0, handleSignals: false });
await once(handle.server, "listening");

const baseUrl = `http://127.0.0.1:${handle.port}`;

Cancellation with request.signal

On Node, ctx.request.signal aborts in two cases:

  • Request timeout. When requestTimeoutMs elapses, the reason is a TimeoutError.
  • Client disconnect. When the client hangs up before the response finished, the reason is an AbortError.

Both cases apply to GET requests, small pre-buffered bodies, and streamed bodies. Bun and Deno abort the platform's own Request.signal on disconnect, so the same handler code works there too. Forward the signal to anything that costs money or holds resources:

ts
app.post("/ask", contract, async ({ body, request }) => {
  // Stops the model call if the user closes the tab or the request times out.
  const { text } = await generateText({
    model,
    prompt: body.prompt,
    abortSignal: request.signal,
  });
  return { status: 200 as const, body: { answer: text } };
});

The adapter attaches its disconnect listener only when a handler reads signal, so routes that never read it pay nothing. A response that finished normally never aborts the signal afterwards. Work that fails because of a disconnect is logged as request.disconnected with disconnectStatusCode (default 499), not as a 5xx.

@daloyjs/core/bun

ts
serve(app: App, opts?: BunServeOptions): BunServerHandle;

interface BunServeOptions {
  port?:               number;
  hostname?:           string;
  maxRequestBodySize?: number;  // default: 16 MiB
  idleTimeout?:        number;
  development?:        boolean;
  unix?:               string;
  tls?:                BunTLSOptions;
}
interface BunServerHandle { port: number; url: URL | undefined; stop(): Promise<void> }

@daloyjs/core/deno

ts
serve(app: App, opts?: DenoServeOptions): DenoServerHandle;

interface DenoServeOptions {
  port?: number; hostname?: string;
  signal?: AbortSignal;
  cert?: string; key?: string;                   // HTTPS pair
  onListen?: (info: { hostname: string; port: number }) => void;
  onError?:  (err: unknown) => Response | Promise<Response>;
  handleSignals?: boolean;                       // default: true
  shutdownTimeoutMs?: number;                    // default: 10_000
}
interface DenoServerHandle { shutdown(): Promise<void> }

@daloyjs/core/cloudflare

ts
toFetchHandler<Env = unknown>(app: App): ExportedFetchHandler<Env>;
  // export default toFetchHandler(app);

interface ExportedFetchHandler<Env = unknown> {
  fetch: (request: Request, env?: Env, ctx?: { waitUntil?; passThroughOnException? }) => Promise<Response>;
}

@daloyjs/core/vercel

ts
type WebHandler = (req: Request) => Promise<Response>;
interface FetchHandler { fetch: WebHandler }
type RouteHandlers = Record<"GET"|"POST"|"PUT"|"PATCH"|"DELETE"|"OPTIONS"|"HEAD", WebHandler>;

toWebHandler   (app: App): WebHandler;        // bare function (middleware, deprecated Edge runtime)
toFetchHandler (app: App): FetchHandler;      // default export for Node Functions
toRouteHandlers(app: App): RouteHandlers;     // Next.js App Router route.ts

@daloyjs/core/fastly

ts
toFastlyHandler(app: App): (req: Request, waitUntil?: (p: Promise<unknown>) => void) => Promise<Response>;
installFastlyListener(app: App): void;   // wires addEventListener("fetch", ...) and event.waitUntil

Pass the fetch event's waitUntil so the OTLP telemetry flush can finish after the response; without it the flush is fire-and-forget.

@daloyjs/core/lambda

ts
toLambdaHandler(app: App): LambdaHandler;
toLambdaStreamHandler(app: App): LambdaStreamHandler;

type LambdaHandler  = (event: LambdaEvent) => Promise<LambdaResponse>;
type LambdaStreamHandler = (event: LambdaEvent, responseStream: LambdaResponseStream, context?: unknown) => Promise<void>;
type LambdaEvent    = LambdaEventV1   | LambdaEventV2;     // API Gateway REST + HTTP/Function URLs
type LambdaResponse = LambdaResponseV1 | LambdaResponseV2;

// A Host that is not a plain host[:port] or [IPv6][:port] (for example one
// containing \ / ? # @ % or whitespace) is refused with 400.
// X-Forwarded-Proto is honoured only as http or https; anything else is https.

@daloyjs/core/azure

ts
toAzureHandler(app: App, opts?: AzureHandlerOptions): AzureHttpHandler;
  // azure.http("api", { route: "{*path}", methods: [...], handler: toAzureHandler(app) })

interface AzureHandlerOptions {
  stream?: boolean;  // return the body as a ReadableStream; pair with app.setup({ enableHttpStream: true })
}
type AzureHttpHandler = (request: AzureHttpRequestLike, context?: unknown) => Promise<AzureHttpResponseInit>;

interface AzureHttpResponseInit {
  status: number;
  headers: Record<string, string>;            // every header except set-cookie
  cookies?: AzureCookie[];                    // one entry per Set-Cookie
  body?: ReadableStream<Uint8Array> | ArrayBuffer;
}

// The request body is passed in as a stream, so bodyLimitBytes is enforced
// while reading. An unparseable request URL is answered with 400.
// Partitioned / Priority cookie attributes have no Azure field and are dropped.
// Azure exposes no TCP peer: getConnInfo() is undefined; use behindProxy.
stream
boolean
Default:-
Return the response body as a ReadableStream instead of buffering it. Enable this only together with app.setup({ enableHttpStream: true }) from @azure/functions; without that host setting Azure buffers the stream anyway. Default: false.
Generated from AzureHandlerOptions in src/adapters/azure.ts.

Test-only / internal helpers

These are exported for internal tests and tooling. They are public-typed but underscore-prefixed. They may change without a semver bump. Most application code will never need them.

ts
_resetCrashHandlersForTests();
_resetInsecureDefaultsLogForTests();
_resetCompressionRuntimeProbeForTests();
_resetSharedRateLimitStoresForTests();