DaloyJS emits a clean OpenAPI 3.1 document straight from your route definitions, no plugins, no separate decorators. Validation, types, and the spec all share one source of truth.
One contract, four outputs
single source of truthRoute definitionapp.route({ request, responses, handler })
You write the route once. Validation, the OpenAPI spec, the interactive docs, and the typed client are all derived from it, so they can never drift out of sync.
One line: auto-mount /docs, /openapi.json, /openapi.yaml
FastAPI-style. Pass docs: true to the App constructor and DaloyJS registers GET /openapi.json + GET /openapi.yaml (the live spec in both formats) and GET /docs (a Scalar API reference UI) for you.
ts
import { App } from "@daloyjs/core";const app = new App({ openapi: { info: { title: "My API", version: "1.0.0" }, servers: [{ url: "https://api.example.com" }], securitySchemes: { bearer: { type: "http", scheme: "bearer" } }, }, docs: true, // mounts GET /docs, GET /openapi.json, GET /openapi.yaml});
Use docs: "auto" to mount only when production: false, or leave it off (the default) and mount manually with the helpers below. Customize paths, UI, and tags via the object form:
Set ui to "scalar" (default), "swagger", or "redoc". All three render the same live spec, mount on the same paths, and ship the same strict CSP and CDN-hosted assets, so switching is a one-word change. Scalar and Swagger UI include a developer request console for protected routes. Redoc is a read-only reference UI: it displays security requirements, but it does not ship a built-in Try it console.
Swagger UI keeps authorizations from its Authorize dialog across reloads by default. Scalar automatically selects the first configured OpenAPI security scheme, so the auth form is ready for developers to paste a bearer token or API key. If you want a different Scalar default, set it explicitly:
The redoc object is forwarded verbatim to Redoc.init(specUrl, configuration, element). Because Redoc builds its search index in a blob: Web Worker, the auto-mounted /docs route automatically widens that page's CSP with worker-src 'self' blob: for ui: "redoc" only. Scalar and Swagger UI keep the tighter policy. The scalar option is ignored unless ui is "scalar", and likewise for swagger and redoc.
The scalar object is forwarded to Scalar's HTML API as JSON configuration while Daloy keeps the live openapiPath as the source. Use it for themes, custom CSS, layout, auth defaults, and client visibility without copying the docs HTML.
Advanced: generate the spec manually
Need the raw spec object (for codegen, contract tests, or a custom route)? Call generateOpenAPI(app, options) directly:
swaggerUiHtml, scalarHtml, and redocHtml all return self-contained HTML pages that load their assets from jsDelivr with a strict CSP allowing only that origin. When you hand-roll the route with redocHtml, pass allowBlobWorkers: true to htmlResponse (or docsContentSecurityPolicy) so Redoc's blob: search worker is allowed by the CSP:
ts
import { redocHtml, htmlResponse } from "@daloyjs/core/docs";const res = htmlResponse( redocHtml({ specUrl: "/openapi.json", title: "My API", configuration: { hideDownloadButtons: true } }), { allowBlobWorkers: true },);
If you want to test your docs UX against a much larger contract, see the large fake REST demo. It is a better benchmark than a toy CRUD sample when you need to validate search, grouping, and render performance.
Dump to disk for codegen
ts
// scripts/dump-openapi.tsimport { writeFile, mkdir } from "node:fs/promises";import { dirname } from "node:path";import { generateOpenAPI } from "@daloyjs/core/openapi";import { buildApp } from "../src/build-app.js";const app = buildApp();const out = "./generated/openapi.json";await mkdir(dirname(out), { recursive: true });await writeFile(out, JSON.stringify(generateOpenAPI(app, { info: { title: "My API", version: "1.0.0" },}), null, 2));console.log(`wrote ${out}`);
One operationId per route, duplicates throw at registration.
Path params :id normalized to {id}.
Schema bodies converted via schema.toJSONSchema?.() when supported, or a structural fallback.
Reusable components.schemas.Problem for RFC 9457 errors.
tags, summary, description, and per-status description.
Webhooks
OpenAPI 3.1 lets a producer publish top-level webhooks , operations a consumer is expected to implement. Pass webhooks to generateOpenAPIand DaloyJS emits them under the document's top-level webhooks map.
Callbacks describe out-of-band requests that an operation may trigger on the consumer (e.g. a subscription endpoint that later POSTs to the URL the caller supplied). Attach a callbacks map directly to a route or webhook.
Each callback name maps to one or more runtime expression keys (e.g. "{$request.body#/callbackUrl}"), each of which maps to one or more operations keyed by HTTP method. Empty maps and empty arrays are skipped, passing an empty callback never produces a malformed spec.
Discriminated unions
OpenAPI 3.1's discriminator is the canonical way to describe tagged unions. DaloyJS ships two helpers from @daloyjs/core/openapi (and the root package):
discriminator(propertyName, mapping?): the bare spec builder. Use it when you already have a hand-rolled JSON Schema and just want to attach the field cleanly.
discriminatedUnion(propertyName, variants, opts?): a Standard-Schema- compatible wrapper that both validates at runtime (dispatching on the discriminator value) and exposes .toJSONSchema() so the OpenAPI generator emits { oneOf, discriminator } automatically.
At runtime the wrapper rejects non-objects, missing or non-string discriminators, and unknown discriminator values with a clear Standard Schema issue, then defers to the matching variant's validator for everything else.