rhythmjs

Search documentation

Search guides, the tutorial and every package.

On this page

Describe routes inline#

OpenAPI metadata is middleware too: annotations sit on the route they describe, and the schema middleware doubles as runtime validation — apiBody and friends validate exactly like validate() while also feeding the generated document:

TypeScript
import { apiOperation, apiTags } from "@rhythmjs/openapi/operation";
import { apiBody } from "@rhythmjs/openapi/body";
import { apiResponse } from "@rhythmjs/openapi/response";

notesController.post(
  "/",
  apiOperation({ summary: "Create a note" }),
  apiTags("notes"),
  apiBody(createNoteSchema),                       // documents AND validates; ctx.valid.body
  apiResponse(201, { schema: noteSchema, description: "The created note" }),
  (ctx) => {
    ctx.json(ctx.notesService.create(ctx.valid.body), 201);
  },
);

apiQuery, apiParam, apiHeader, and apiCookie cover the other inputs with the same validate-and-document behavior.

Serve the document#

openapiModule finds the routers mounted in your app, generates the spec from their annotations, and serves it as JSON; scalarModule renders a browsable UI over it:

TypeScript
import { openapiModule } from "@rhythmjs/openapi/module";
import { scalarModule } from "@rhythmjs/scalar";
import { defineDocument } from "@rhythmjs/openapi/document";

const config = defineDocument({
  openapi: "3.1.0",
  info: { title: "Notes API", version: "1.0.0" },
});

appModule
  .register(openapiModule.forRoot({ document: config }))     // GET /openapi.json
  .register(scalarModule.forRoot({ pageTitle: "Notes API" }))    // GET /docs
  .use(notesController.middleware());

Document security#

apiBearerAuth(), apiBasicAuth(), apiKeyAuth(), and apiOAuth2() record a route's security requirements in the spec. They are documentation only — they never enforce anything. Enforcement stays with the security middleware from step 8, so a protected route carries both:

TypeScript
import { apiBearerAuth } from "@rhythmjs/openapi/security";

notesController.delete(
  "/:id",
  apiBearerAuth(),                       // documents the requirement
  requireAuthentication(),               // enforces it
  requireRoles(["admin"]),
  (ctx) => { /* ... */ },
);