OpenAPI generation
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.
Generate a spec
import { z } from "zod";
import { generateOpenAPI } from "@daloyjs/core/openapi";
const doc = generateOpenAPI(app, {
info: { title: "My API", version: "1.0.0" },
servers: [{ url: "https://api.example.com" }],
securitySchemes: {
bearer: { type: "http", scheme: "bearer" },
},
});
console.log(JSON.stringify(doc, null, 2));Serve the spec from your app
app.route({
method: "GET",
path: "/openapi.json",
operationId: "getOpenAPI",
tags: ["Meta"],
responses: { 200: { description: "OpenAPI 3.1 doc" } },
handler: async () => ({ status: 200, body: generateOpenAPI(app, { info: { title: "My API", version: "1.0.0" } }) }),
});Built-in docs UIs
import { swaggerUiHtml, scalarHtml, htmlResponse } from "@daloyjs/core/docs";
app.route({
method: "GET",
path: "/docs",
operationId: "docs",
responses: { 200: { description: "API reference" } },
handler: async () => {
const html = swaggerUiHtml({ specUrl: "/openapi.json", title: "My API" });
const res = htmlResponse(html);
return { status: 200, body: await res.text(), headers: Object.fromEntries(res.headers) };
},
});Both swaggerUiHtml and scalarHtml return self-contained HTML pages that load their assets from jsDelivr with a strict CSP allowing only that origin. The official starter usesswaggerUiHtml for /docs by default.
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
// scripts/dump-openapi.ts
import { 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}`);// package.json
"scripts": {
"gen:openapi": "node --import tsx/esm scripts/dump-openapi.ts"
}What gets emitted
- One
operationIdper route — duplicates throw at registration. - Path params
:idnormalized to{id}. - Schema bodies converted via
schema.toJSONSchema?.()when supported, or a structural fallback. - Reusable
components.schemas.Problemfor RFC 9457 errors. tags,summary,description, and per-statusdescription.
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.
import { generateOpenAPI } from "@daloyjs/core/openapi";
const doc = generateOpenAPI(app, {
info: { title: "Books", version: "1.0.0" },
webhooks: {
bookCreated: {
method: "POST",
operationId: "onBookCreated",
summary: "Fires when a book is created",
tags: ["Webhooks"],
request: { body: z.object({ id: z.string(), title: z.string() }) },
responses: { 200: { description: "Acknowledged" } },
auth: { scheme: "bearer", scopes: ["webhook:receive"] },
},
},
});Callbacks
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.
app.route({
method: "POST",
path: "/subscribe",
operationId: "subscribe",
request: { body: z.object({ callbackUrl: z.string().url() }) },
responses: { 201: { description: "Subscribed" } },
callbacks: {
onEvent: {
"{$request.body#/callbackUrl}": {
method: "POST",
operationId: "onEventCallback",
request: { body: z.object({ id: z.string() }) },
responses: {
200: { description: "ack" },
410: { description: "gone" },
},
},
},
},
handler: async () => ({ status: 201, body: undefined }),
});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.
import { z } from "zod";
import { discriminatedUnion } from "@daloyjs/core";
const Cat = z.object({ kind: z.literal("cat"), meow: z.boolean() });
const Dog = z.object({ kind: z.literal("dog"), bark: z.boolean() });
const Animal = discriminatedUnion(
"kind",
{ cat: Cat, dog: Dog },
{ mapping: { cat: "#/components/schemas/Cat", dog: "#/components/schemas/Dog" } },
);
app.route({
method: "POST",
path: "/animals",
operationId: "createAnimal",
request: { body: Animal },
responses: { 201: { description: "ok", body: Animal } },
handler: async ({ body }) => ({ status: 201, body }),
});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.