Skip to content

OpenAPI and Scalar

Rizky Zulkarnaen edited this page Sep 9, 2026 · 1 revision

OpenAPI and Scalar

Source of truth: docs/openapi.md · ADR: ADR-0025

Generation-first OpenAPI 3.1 plus an optional Scalar reference UI — with zero new npm dependencies (Scalar loads from its public CDN in a generated HTML shell). The JSON document is canonical; Scalar is a replaceable presentation layer.

import { defineApp, json, route } from "lugas";

defineApp({
  openapi: {
    document: {
      title: "Store API",
      version: "1.0.0",
      description: "Product catalog and orders",
    },
    path: "/openapi.json",   // default
    ui: { path: "/docs" },   // or ui: true → "/docs"; omit for no UI
  },
  routes: {
    "/products/:id": {
      GET: route({
        openapi: {
          summary: "Get product details",
          operationId: "getProduct",
          tags: ["Products"],
        },
        handler: (ctx) => json(200, { id: ctx.params.id }),
      }),
    },
  },
});

Where the document comes from

  1. Prepared graph facts — paths, HTTP methods, module names (as tags). Bun path syntax converts: /items/:id/items/{id}.
  2. Standard Schema declarations (params, query, headers, body), with feature-detected Standard JSON Schema: a validator exposing ~standard.jsonSchema() has its schema used verbatim; validators without a representation document presence only (requiredness from declared position, body as type: object) — never a guessed shape.
  3. route({ openapi }) metadatasummary, description, operationId, tags, deprecated, explicit responses, and schema overrides for params/query/headers/body.
  4. RFC 9457 Problem Details registered as #/components/schemas/ProblemDetails and documented as every operation's default error response — the framework's real error envelope.

The document is generated once at defineApp() time (the routing graph is snapshotted by contract), so what the document says cannot drift from what serves.

Endpoints are framework handlers

The document and UI handlers pass through the traffic gate, error policy, Structured Logging, and CORS wrappers like any route, and appear in lugas-manifest-v1 as ordinary facts.

Conflicts fail closed

  • openapi.path or ui.path colliding with an existing route or assets.files mapping → LUGAS_OPENAPI_002 at startup.
  • Self-collision between document and UI paths, or malformed config → LUGAS_OPENAPI_001.
  • Absent openapi config means no endpoints, no files, nothing.

Production exposure

ui is opt-in by design. The shell loads Scalar's script from the public CDN — a third-party runtime fetch — so production deployments may prefer to gate the route, keep it development-only, or replace the shell entirely (the JSON is the contract).

Pinned by tests/openapi/; evidence in docs/reports/issues/M8-004.md.

Clone this wiki locally