app
js/net/http/app.ts
fino:net/http/app — middleware, routing, and OpenAPI for HTTP services.
This module builds a small application framework on top of Fino's existing
Request, Response, and serve() APIs. It is intended for APIs that need
composable middleware, URLPattern routing, request-scoped values, and a
machine-readable OpenAPI document without giving up direct access to the
underlying Fetch-compatible HTTP primitives.
The middleware model has two explicit forms. use() installs one-way branch
middleware that may short-circuit by returning a Response or continue by
returning nothing. layer() installs Koa-style wrappers that receive
(ctx, next) and can run before and after downstream dispatch. Builders
(App, Router, RouteBuilder, and MethodBuilder) share .use(),
.layer(), .value(), and .meta() as immutable enrichments of a routing
tree: each call records a node, terminals such as .handle() resolve the
branch they hang off, and a later value() with the same name shadows an
earlier one. Everything that binds a routing path goes through route() —
HTTP verbs fork method branches finished by .handle(), while websocket(),
sse(), webtransport(), rpc(), and mount() are terminals that register
directly.
Routing uses the platform URLPattern implementation with pathname patterns
such as /users/:id. Path parameters are available through the built-in
schema.params() producer, which reserves the usual params slot and
validates the matched parameter object. app.context() returns the active
request context from anywhere in the async call chain by using
fino:context.
OpenAPI generation targets OpenAPI 3.1 and embeds JSON Schema objects from
fino:validate directly. Middleware can describe documentation effects with
defineMiddleware(fn, meta) and defineProducer(fn, meta), making runtime
logic independent from documentation generation.
import { App, body, schema } from 'fino:net/http/app';
import { v } from 'fino:validate';
const app = new App({ name: 'Example API' });
const api = app.layer(errorHandler());
api.route('/users/:id')
.meta({ tags: ['users'] })
.value('params', schema.params(v.object({ id: v.string() })))
.post()
.value('body', body.json(v.object({ name: v.string() })))
.handle((ctx) => Response.json({ id: ctx.params.id, name: ctx.body.name }));
app.get('/openapi.json').handle(app.openapiHandler({ version: '1.0.0' }));
app.listen({ port: 3000 });
Learn more:
- OpenAPI 3.1: https://spec.openapis.org/oas/v3.1.0
- URLPattern: https://wicg.github.io/urlpattern/
- HTTP cookies: https://www.rfc-editor.org/rfc/rfc6265
Types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
Standard HTTP methods supported by route builders.
const method: HttpMethod = 'GET';
type Middleware = (ctx: HttpContext) => HttpHandlerResult | void | Promise<HttpHandlerResult | void>
One-way branch middleware run before downstream handlers.
Each middleware receives the shared request context. Returning a
HttpHandlerResult short-circuits the chain and skips everything downstream;
returning undefined (or nothing) continues to the next branch item. Use
LayerMiddleware with .layer() when code needs a next callback or needs
to inspect the downstream response.
const requireUser: Middleware = async (ctx) => {
if (ctx.user === undefined) return Response.json({ error: 'Unauthorized' }, { status: 401 });
};
app.route('/account').use(requireUser).get().handle(showAccount);
type LayerMiddleware = (
ctx: HttpContext,
next: (
) => Promise<HttpHandlerResult>
) => HttpHandlerResult | void | Promise<HttpHandlerResult | void>
Koa-style wrapper layer run around downstream dispatch.
A layer receives the shared request context and a next callback. Calling
await next() continues dispatch and yields the downstream result, which the
layer may inspect or mutate before returning. Returning a response without
calling next() short-circuits the rest of dispatch.
next() must be called at most once per invocation — calling it a second
time throws next() called multiple times.
const timing: LayerMiddleware = async (ctx, next) => {
const res = await next();
if (res instanceof Response) res.headers.set('x-route', ctx.route);
return res;
};
app.layer(timing);
type Handler = (ctx: HttpContext) => HttpHandlerResult | Promise<HttpHandlerResult>
Terminal route handler.
Handlers must return a Response or a protocol takeover such as a WebSocket
connection. Throwing is normally handled by errorHandler().
const handler: Handler = (ctx) => Response.json({ id: ctx.params?.id });
type WebSocketHandler = (socket: WebSocketConnection, ctx: WebSocketContext) => void | Promise<void>
Terminal handler for a WebSocket route.
The upgrade is already accepted by the time the handler runs, so it receives
a live WebSocketConnection and the request context. Register event
listeners and start reading before the handler returns; returning does not
close the socket, so keep the promise pending only as long as setup requires.
const echo: WebSocketHandler = (socket) => {
socket.addEventListener('message', (event) => socket.send(event.data));
};
app.route('/echo').websocket(echo);
type WebTransportHandler = (session: WebTransport, ctx: WebTransportContext) => void | Promise<void>
Terminal handler for a WebTransport route.
Runs after the session upgrade is accepted, receiving a live WebTransport
session and the request context. Await session.ready before opening or
accepting streams; as with WebSocket handlers, returning does not close the
session.
const handler: WebTransportHandler = async (session) => {
await session.ready;
};
app.route('/wt').webtransport(handler);
type SseHandler = (events: EventSourceWriter, ctx: SseContext) => void | Promise<void>
Terminal handler for a server-sent events route.
The handler receives an EventSourceWriter connected to the response body.
The response streams while the handler runs and ends when it returns. Unlike
WebSocket and WebTransport handlers, there is no protocol upgrade to accept:
SSE is a normal HTTP response with a long-lived body.
const clock: SseHandler = async (events) => {
await events.write({ event: 'tick', data: new Date().toISOString() });
};
app.route('/clock').sse(clock);
type Producer = (ctx: HttpContext) => unknown | Promise<unknown>
Context value producer used by .value(name, producer).
The producer runs at its position in the middleware stack and stores its return value under the declared context key.
const currentUser: Producer = async (ctx) => loadUser(ctx.request);
type HttpHandlerResult = Response | WebSocketConnection | WebTransport
Re-exported from js/net/http/server.HttpHandlerResult.
Interfaces
interface HttpContext {
Request context object passed through app middleware and handlers.
Producers can add additional keys, so application-specific context values are
exposed through the index signature. Built-in helpers commonly add params,
query, headers, body, cookies, and session.
const handler: Handler = (ctx) => Response.json({ route: ctx.route });
Properties
request: Request
Fetch-compatible request being handled.
console.log(ctx.request.method);
app: App
App instance that is dispatching this request.
console.log(ctx.app.context());
route: string
Matched route pathname pattern, such as /users/:id.
console.log(ctx.route);
method: string
Matched HTTP method.
console.log(ctx.method);
protocol: HttpProtocol
Protocol carrying the current request.
session?: HttpSession
Transport session carrying the current request.
incoming?: IncomingHttp
Accept object that produced this request, when dispatched by App.listen().
params?: Record<string, string>
URLPattern path parameters when a route matched.
console.log(ctx.params?.id);
interface WebSocketContext extends HttpContext {
Request context passed to WebSocket route handlers.
Identical to HttpContext except that incoming is narrowed to the pending
WebSocket upgrade, which the route framework accepts on the handler's behalf
before the handler runs. Middleware on the branch still sees the request as a
plain HttpContext, so producers and values populate this context the same
way they do for HTTP routes.
app.route('/chat').websocket((socket, ctx: WebSocketContext) => {
console.log('upgraded', ctx.route, ctx.params);
socket.addEventListener('message', (e) => socket.send(e.data));
});
Properties
incoming: IncomingWebSocketRequest
Pending WebSocket upgrade the route accepted to produce socket.
interface WebTransportContext extends HttpContext {
Request context passed to WebTransport route handlers.
Like WebSocketContext, this narrows incoming to the pending WebTransport
upgrade the route accepts before the handler runs. All other context values
— matched route, params, and anything added by producers on the branch —
are populated exactly as they are for HTTP routes.
app.route('/wt/:room').webtransport(async (session, ctx: WebTransportContext) => {
console.log('room', ctx.params?.room);
await session.ready;
});
Properties
incoming: IncomingWebTransportRequest
Pending WebTransport upgrade the route accepted to produce session.
interface SseContext extends HttpContext {
Request context passed to server-sent events route handlers.
SSE is not an HTTP upgrade. It is an ordinary HTTP request whose response
body remains open with text/event-stream, so incoming is narrowed to the
plain request variant when the app is dispatching from listen(). Middleware
can still reject before the SSE stream starts by returning a Response.
app.route('/events').sse(async (events, ctx: SseContext) => {
console.log(ctx.request.headers.get('last-event-id'));
await events.write({ data: 'connected' });
});
Properties
incoming?: IncomingHttpRequest
Incoming plain HTTP request, when dispatched by App.listen().
This is not an upgrade request; it has already been accepted as ordinary HTTP before the route handler starts streaming.
interface OperationMeta {
OpenAPI metadata that can be attached to builders, middleware, or producers.
Metadata is merged as routes are built. Later metadata overrides earlier
metadata key by key: parameters replace by (in, name), request bodies and
per-status responses replace outright.
app.route('/users').meta({ tags: ['users'], summary: 'List users' });
Properties
operationId?: string
Explicit OpenAPI operationId.
route.meta({ operationId: 'getUser' });
summary?: string
Short OpenAPI summary.
route.meta({ summary: 'Create a user' });
description?: string
Longer OpenAPI description.
route.meta({ description: 'Creates a user account.' });
tags?: string[]
OpenAPI tags for grouping operations.
route.meta({ tags: ['users'] });
security?: unknown
OpenAPI security requirement object or array.
route.meta({ security: [{ bearerAuth: [] }] });
parameters?: OpenApiParameter[]
Additional OpenAPI parameters.
route.meta({ parameters: [{ name: 'id', in: 'path', required: true }] });
requestBody?: OpenApiRequestBody
OpenAPI requestBody metadata.
route.meta({ requestBody: { required: true, content: { 'application/json': { schema } } } });
responses?: Record<string, OpenApiResponse>
OpenAPI responses keyed by status code.
route.meta({ responses: { '200': { description: 'OK' } } });
interface OpenApiOptions {
Options passed to App.openapi().
version is required and becomes info.version. title defaults to the app
name passed to new App().
const doc = app.openapi({ version: '1.0.0', title: 'Admin API' });
Properties
title?: string
OpenAPI info title; defaults to the app name.
app.openapi({ title: 'Example API', version: '1.0.0' });
version: string
OpenAPI info version.
app.openapi({ version: '1.0.0' });
servers?: Array<Record<string, unknown>>
Optional OpenAPI servers array.
app.openapi({ version: '1.0.0', servers: [{ url: 'https://api.example.com' }] });
interface OpenApiParameter {
OpenAPI parameter object accepted by OperationMeta.parameters.
Parameters are merged by (in, name) when metadata from builders,
middleware, and producers is combined. schema should be an OpenAPI 3.1 JSON
Schema object or a fino:validate schema converted through toJSON().
const parameter: OpenApiParameter = {
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
};
Properties
name: string
Parameter name as it appears in the path, query string, header, or cookie.
in: 'path' | 'query' | 'header' | 'cookie'
Location of the parameter in the HTTP request.
required?: boolean
Whether the parameter is required. Path parameters should set this true.
schema?: unknown
OpenAPI 3.1 schema for the parameter value.
description?: string
Human-readable parameter description.
interface OpenApiRequestBody {
OpenAPI request body object accepted by OperationMeta.requestBody.
The content map is keyed by media type. Each entry carries a schema object
that is copied into the generated OpenAPI document.
const body: OpenApiRequestBody = {
required: true,
content: {
'application/json': { schema: { type: 'object' } },
},
};
Properties
required?: boolean
Whether the request body is required.
description?: string
Human-readable request body description.
content: Record<string, {
schema: unknown;
}>
Media-type map for request body schemas.
interface OpenApiResponse {
OpenAPI response object accepted by OperationMeta.responses.
Responses are keyed by status code in OperationMeta.responses. content is
optional for status codes that do not return a body.
const response: OpenApiResponse = {
description: 'OK',
content: {
'application/json': { schema: { type: 'object' } },
},
};
Properties
description: string
Required human-readable response description.
content?: Record<string, {
schema: unknown;
}>
Optional media-type map for response body schemas.
interface Session {
Session data persisted by a SessionStore.
session.data.userId = 'u_123';
Properties
id: string
Stable session ID.
console.log(session.id);
data: Record<string, unknown>
Mutable session payload.
session.data.count = Number(session.data.count ?? 0) + 1;
isNew: boolean
True when this session was created for the current request.
if (session.isNew) console.log('new session');
interface SessionStore {
Minimal async session store interface.
Store methods may be synchronous or async. Returned sessions should be safe for request-local mutation.
const store = memorySessionStore();
Methods
get(id: string): Session | null | Promise<Session | null>
Load a session by ID, or return null when missing.
const session = await store.get(id);
set(id: string, session: Session): void | Promise<void>
Persist a session by ID.
await store.set(session.id, session);
delete(id: string): void | Promise<void>
Delete a session by ID.
await store.delete(id);
Classes
abstract class BuilderBranch<TSelf extends BuilderBranch<TSelf>> {
Methods
use(...middleware: Middleware[]): TSelf
Return a new builder with one-way middleware appended to this branch.
The middleware runs only when dispatch is flowing toward a candidate terminal below this branch. Returning a response short-circuits; returning nothing continues.
const authed = app.use(requireUser);
authed.get('/account').handle(showAccount);
layer(...layers: LayerMiddleware[]): TSelf
Return a new builder with wrapper layers appended to this branch.
Layers receive (ctx, next) and may wrap downstream dispatch, including
fallback responses when this branch's routing constraints match.
const logged = app.layer(accessLog);
value(name: string, producer: Producer): TSelf
Return a new builder with a context value appended to this branch.
const withSession = app.value('session', sessions({ store }));
meta(meta: OperationMeta): TSelf
Return a new builder with OpenAPI metadata appended to this branch.
const users = app.meta({ tags: ['users'] });
abstract class RouterBase<TSelf> extends BuilderBranch<RouterBranch> {
Shared route-container surface for App, Router, and immutable root
branches.
Container builders can start path branches with route() or path-taking verb
shortcuts. Enrichments such as use(), layer(), value(), and meta() are
inherited from BuilderBranch and return immutable branch builders; they do
not mutate the original container.
Constructors
constructor()
Methods
route(path: string): RouteBuilder
Start a route branch for one URLPattern pathname.
app.route('/users/:id').get().handle((ctx) => Response.json(ctx.params));
get(path: string, ...rest: never[]): MethodBuilder
Start a GET method branch; register the handler with .handle().
app.get('/health').handle(() => Response.json({ ok: true }));
post(path: string, ...rest: never[]): MethodBuilder
Start a POST method branch; register the handler with .handle().
app.post('/users').handle((ctx) => Response.json({}, { status: 201 }));
put(path: string, ...rest: never[]): MethodBuilder
Start a PUT method branch; register the handler with .handle().
app.put('/users/:id').handle((ctx) => Response.json(ctx.params));
patch(path: string, ...rest: never[]): MethodBuilder
Start a PATCH method branch; register the handler with .handle().
app.patch('/users/:id').handle((ctx) => Response.json(ctx.params));
delete(path: string, ...rest: never[]): MethodBuilder
Start a DELETE method branch; register the handler with .handle().
app.delete('/users/:id').handle(() => new Response(null, { status: 204 }));
head(path: string, ...rest: never[]): MethodBuilder
Start a HEAD method branch; register the handler with .handle().
app.head('/health').handle(() => new Response(null, { status: 204 }));
options(path: string, ...rest: never[]): MethodBuilder
Start an OPTIONS method branch; register the handler with .handle().
app.options('/users').handle(() => new Response(null, { status: 204 }));
class RouterBranch extends BuilderBranch<RouterBranch> {
Immutable app/router branch with path-taking route helpers.
Instances are returned by root-level enrichments such as app.use(auth) or
router.layer(log). Hold the returned branch to register multiple routes
under the same inherited chain.
const authed = app.use(requireUser);
authed.get('/account').handle(showAccount);
authed.post('/logout').handle(logout);
Methods
route(path: string): RouteBuilder
Start a route branch for one URLPattern pathname.
branch.route('/users/:id').get().handle((ctx) => Response.json(ctx.params));
get(path: string, ...rest: never[]): MethodBuilder
Start a GET method branch; register the handler with .handle().
post(path: string, ...rest: never[]): MethodBuilder
Start a POST method branch; register the handler with .handle().
put(path: string, ...rest: never[]): MethodBuilder
Start a PUT method branch; register the handler with .handle().
patch(path: string, ...rest: never[]): MethodBuilder
Start a PATCH method branch; register the handler with .handle().
delete(path: string, ...rest: never[]): MethodBuilder
Start a DELETE method branch; register the handler with .handle().
head(path: string, ...rest: never[]): MethodBuilder
Start a HEAD method branch; register the handler with .handle().
options(path: string, ...rest: never[]): MethodBuilder
Start an OPTIONS method branch; register the handler with .handle().
class App extends RouterBase<App> {
HTTP application with middleware, routes, async context, serving, and docs.
const app = new App({ name: 'Example API' });
app.get('/').handle(() => new Response('ok'));
Constructors
constructor(options: {
name?: string;
} = {})
Methods
context(): HttpContext | undefined
Return the active request context from anywhere in the async call chain.
const ctx = app.context();
async handle(req: Request, info: HandleInfo = {}): Promise<HttpHandlerResult>
Dispatch one request through the matching operation chain.
A path that matches with no matching method returns 405 with an Allow
header. A path served only by WebSocket or WebTransport operations returns
426 Upgrade Required. Otherwise unmatched requests return a JSON 404 after
running the app-level middleware chain.
const response = await app.handle(new Request('http://local/health'));
listen(options: Parameters<typeof serve>[0]): ReturnType<typeof serve>
Start an HTTP server that dispatches requests to this app.
The returned server is the same object returned by serve().
const server = app.listen({ port: 3000 });
openapi(options: OpenApiOptions): Record<string, unknown>
Generate an OpenAPI 3.1 document from registered operations and metadata.
HTTP and SSE operations are documented; SSE paths emit GET and POST
operations with a text/event-stream response. WebSocket and WebTransport
operations are not part of the OpenAPI surface. Throws when generated or
explicit operation IDs collide.
const doc = app.openapi({ version: '1.0.0' });
openapiHandler(options: OpenApiOptions): Handler
Return a handler that serves this app's OpenAPI document as JSON.
app.get('/openapi.json').handle(app.openapiHandler({ version: '1.0.0' }));
class Router extends RouterBase<Router> {
Reusable route collection mountable under a route prefix.
Mounting resolves the router's operations into the target; a router cannot be changed after it has been mounted.
const router = new Router();
router.get('/users').handle(() => Response.json([]));
app.route('/api').mount(router);
Methods
override _assertMutable(): void
class RouteBuilder extends BuilderBranch<RouteBuilder> {
Builder for one URLPattern pathname and its enrichment branch.
Enrichment methods return a new builder; a held reference is a fixed point
in the routing tree, so chain or reassign to accumulate. Verb methods start
HTTP method branches finished by .handle(); websocket(), sse(),
webtransport(), rpc(), and mount() are terminals that register
directly.
app.route('/users/:id').meta({ tags: ['users'] }).get().handle((ctx) => Response.json(ctx.params));
Methods
use(...middleware: Middleware[]): RouteBuilder
Return a new builder with middleware appended to this branch.
app.route('/admin').use(requireAdmin).get().handle(showAdmin);
layer(...layers: LayerMiddleware[]): RouteBuilder
Return a new builder with wrapper layers appended to this branch.
app.route('/admin').layer(auditLog).get().handle(showAdmin);
value(name: string, producer: Producer): RouteBuilder
Return a new builder with a context value appended to this branch.
app.route('/users/:id').value('params', schema.params(idSchema));
meta(meta: OperationMeta): RouteBuilder
Return a new builder with OpenAPI metadata appended to this branch.
app.route('/users').meta({ tags: ['users'] });
route(path: string): RouteBuilder
Start a nested route branch under this one.
The nested path appends to this route's path and the nested branch inherits everything accumulated above it.
const users = app.route('/users');
users.get().handle(listUsers);
users.route('/:id').get().handle(showUser);
get(...rest: never[]): MethodBuilder
Start a GET method branch on this route.
app.route('/items').get().handle(() => Response.json([]));
post(...rest: never[]): MethodBuilder
Start a POST method branch on this route.
app.route('/items').post().value('body', body.json()).handle((ctx) => Response.json(ctx.body));
put(...rest: never[]): MethodBuilder
Start a PUT method branch on this route.
app.route('/items/:id').put().handle((ctx) => Response.json(ctx.params));
patch(...rest: never[]): MethodBuilder
Start a PATCH method branch on this route.
app.route('/items/:id').patch().handle((ctx) => Response.json(ctx.params));
delete(...rest: never[]): MethodBuilder
Start a DELETE method branch on this route.
app.route('/items/:id').delete().handle(() => new Response(null, { status: 204 }));
head(...rest: never[]): MethodBuilder
Start a HEAD method branch on this route.
app.route('/items').head().handle(() => new Response(null, { status: 204 }));
options(...rest: never[]): MethodBuilder
Start an OPTIONS method branch on this route.
app.route('/items').options().handle(() => new Response(null, { status: 204 }));
websocket(handler: WebSocketHandler): RouteBuilder
Register a WebSocket operation at this route. Terminal.
Middleware on the branch runs before the upgrade is accepted and can
reject it by returning a Response.
app.route('/chat').websocket(async (socket, ctx) => {
socket.addEventListener('message', (event) => socket.send(event.data));
});
webtransport(handler: WebTransportHandler): RouteBuilder
Register a WebTransport operation at this route. Terminal.
app.route('/wt').webtransport(async (session, ctx) => {
await session.ready;
});
sse(handler: SseHandler): RouteBuilder
Register a server-sent events operation at this route. Terminal.
The handler receives an EventSourceWriter wired to the response body.
The route responds with text/event-stream immediately, streams every
event the handler writes, and ends the stream when the handler returns.
This uses the ordinary HTTP request path rather than the WebSocket or
WebTransport upgrade path.
The operation matches GET (for EventSource clients) and POST (for
fetch-based clients that send a request body).
app.route('/events').sse(async (events, ctx) => {
await events.write({ data: 'connected' });
});
rpc(service: {
httpHandler(): (req: Request) => Promise<Response>;
}): RouteBuilder
Mount a JSON-RPC service at this route. Terminal.
POST requests dispatch as JSON-RPC 2.0 messages; other methods return 405
with an Allow header.
import { JsonRpcService } from 'fino:jsonrpc';
const svc = new JsonRpcService();
svc.method('add').handle((p) => (p as { a: number; b: number }).a + (p as { a: number; b: number }).b);
app.route('/rpc').rpc(svc);
mount(router: Router): RouteBuilder
Mount a router's operations under this route. Terminal.
The router's operations are resolved into the owning container with this route's path prefixed and this branch's chain prepended. The router cannot be changed afterwards.
const api = new Router();
api.get('/users').handle(() => Response.json([]));
app.route('/v1').mount(api);
class MethodBuilder extends BuilderBranch<MethodBuilder> {
Builder for one HTTP method branch. Call .handle() to register.
app.route('/items').post().value('body', body.json()).handle((ctx) => Response.json(ctx.body));
Methods
use(...middleware: Middleware[]): MethodBuilder
Return a new builder with middleware appended to this method branch.
app.route('/items').post().use(rateLimit).handle(createItem);
layer(...layers: LayerMiddleware[]): MethodBuilder
Return a new builder with wrapper layers appended to this method branch.
app.route('/items').get().layer(cacheHeaders).handle(listItems);
value(name: string, producer: Producer): MethodBuilder
Return a new builder with a context value appended to this method branch.
app.route('/items').post().value('body', body.json());
meta(meta: OperationMeta): MethodBuilder
Return a new builder with OpenAPI metadata appended to this method branch.
app.route('/items').get().meta({ summary: 'List items' });
handle(handler: Handler): RouteBuilder
Register the handler for this method branch. Terminal.
Resolves the branch into a frozen operation and returns the parent route builder for further registrations on the same route.
app.route('/items').get().handle(() => Response.json([]));
class CookieJar {
Re-exported from cookie.CookieJar.
Functions
function defineMiddleware<T extends Middleware | LayerMiddleware>(
fn: T,
meta: OperationMeta = {
}
): T
Attach static OpenAPI metadata to a middleware or layer function.
The returned function is the original function with non-enumerable metadata attached. Metadata is read when the middleware is installed in a route stack.
const auth = defineMiddleware((ctx) => {
if (ctx.user === undefined) return Response.json({ error: 'Unauthorized' }, { status: 401 });
}, {
security: [{ bearerAuth: [] }],
});
function defineProducer<T extends Producer>(fn: T, meta: OperationMeta = {}): T
Attach static OpenAPI metadata to a context value producer.
The returned producer is the original function with metadata attached. Use this for reusable body, params, query, or session producers.
const user = defineProducer(async (ctx) => loadUser(ctx), {
parameters: [{ name: 'user-id', in: 'header' }],
});
function cookies(): Producer
Producer that creates a CookieJar and appends queued cookies downstream.
Use with .value('cookies', cookies()) so handlers can read and mutate
cookies through ctx.cookies.
app.value('cookies', cookies());
function memorySessionStore(): SessionStore
Create an in-memory session store suitable for tests and single-process apps.
Data is lost when the process exits and is not shared across workers.
const store = memorySessionStore();
app.value('session', sessions({ store }));
function sessions(opts: {
store: SessionStore;
cookie?: string;
cookieOptions?: CookieOptions;
}): Producer
Producer that loads a cookie-backed session and saves it after response creation.
Expects ctx.cookies to be a CookieJar when installed after
.value('cookies', cookies()); otherwise it creates a private jar. New
sessions are assigned UUIDs and persisted after the response is produced.
app.value('cookies', cookies()).value('session', sessions({ store: memorySessionStore() }));
function errorHandler(opts: {
expose?: boolean;
} = {}): LayerMiddleware
Middleware that converts uncaught errors to a JSON error response.
By default, error messages are hidden. Pass { expose: true } for development
or trusted internal APIs.
app.layer(errorHandler({ expose: false }));
function staticFiles(root: string, opts: {
index?: string;
prefix?: string;
} = {}): LayerMiddleware
Serve files from a local root directory, short-circuiting matched requests.
The request path must start with opts.prefix (default /). Paths are
normalized and .. traversal is rejected with 403. Missing files fall
through to downstream middleware.
app.layer(staticFiles('/var/www', { index: 'index.html', prefix: '/' }));
Constants
const schema
Validation and OpenAPI helpers for parameters and responses.
These helpers wrap fino:validate schemas and attach matching OpenAPI
metadata. Runtime validation failures throw from middleware or producer
execution.
app.get('/users').use(schema.query(v.object({ q: v.string() }))).handle((ctx) => Response.json(ctx.query));
Methods
params(schemaValue: unknown): Producer
Validate URLPattern path parameters and expose them as ctx.params.
The returned producer parses ctx.params against the schema and stores the
validated object back under the reserved params slot; install it with
.value('params', schema.params(...)). The schema's object properties are
emitted as required OpenAPI path parameters. Throws from producer
execution when a parameter is missing or fails validation.
app.route('/users/:id').value('params', schema.params(v.object({ id: v.string() })));
query(schemaValue: unknown): Middleware
Validate the request query string and expose it as ctx.query.
The returned middleware parses the URL search parameters into an object
(repeated keys become arrays), validates it, and assigns the result to
ctx.query. Properties become OpenAPI query parameters, required when the
schema marks them required. Throws when validation fails.
app.get('/search').use(schema.query(v.object({ q: v.string() }))).handle((ctx) => Response.json(ctx.query));
headers(schemaValue: unknown): Middleware
Validate request headers and expose them as ctx.headers.
Header names are lowercased before validation and in the generated OpenAPI
header parameters. The validated object is assigned to ctx.headers.
Throws when a required header is missing or malformed.
app.get('/me').use(schema.headers(v.object({ authorization: v.string() }))).handle((ctx) => Response.json(ctx.headers));
response(schemaValue: unknown, opts: {
status?: number;
description?: string;
contentType?: string;
} = {}): LayerMiddleware
Validate the outgoing response body and document it in OpenAPI.
The returned middleware runs downstream, then — only when the response
status and content type match opts (defaults: status 200,
application/json) — clones the response and validates its parsed body,
throwing on mismatch. The response itself is passed through unchanged. The
schema is recorded as the response body schema for the given status.
app.get('/users/:id')
.layer(schema.response(v.object({ id: v.string() }), { status: 200 }))
.handle((ctx) => Response.json({ id: ctx.params?.id }));
const body
Request body producers for .value('body', body.json(...)) and friends.
Body producers consume the request body exactly once. Install them at the point in the stack where the parsed body should become available.
app.post('/items').value('body', body.json()).handle((ctx) => Response.json(ctx.body));
Methods
json(schemaValue?: unknown): Producer
Parse the request body as JSON, optionally validating it against a schema.
The producer stores the parsed value under the slot it is bound to
(conventionally body). When a schema is passed, the parsed value is
validated and the validated value is stored. Throws when the request
declares a content type other than application/json, and when the body is
not valid JSON or fails schema validation.
app.post('/items').value('body', body.json(v.object({ name: v.string() }))).handle((ctx) => Response.json(ctx.body));
text(): Producer
Read the request body as a UTF-8 string.
The producer resolves to the decoded body text and documents a required
text/plain request body in OpenAPI.
app.post('/notes').value('body', body.text()).handle((ctx) => new Response(ctx.body as string));
bytes(): Producer
Read the request body as raw bytes.
The producer resolves to a Uint8Array and documents a required
application/octet-stream binary request body in OpenAPI.
app.put('/blob').value('body', body.bytes()).handle((ctx) => Response.json({ size: (ctx.body as Uint8Array).byteLength }));
form(): Producer
Parse a URL-encoded or multipart form body into FormData.
The producer resolves to the request's FormData and documents both
application/x-www-form-urlencoded and multipart/form-data request
bodies. Throws when the underlying request does not support formData().
app.post('/upload').value('body', body.form()).handle((ctx) => Response.json({ ok: true }));