Search docs

Jump between documentation pages.

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

ts
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

ts
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

ts
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

ts
// 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}`);
json
// package.json
"scripts": {
  "gen:openapi": "node --import tsx/esm scripts/dump-openapi.ts"
}

What gets emitted

  • 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.

ts
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.

ts
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.
ts
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.