rhythmjs

Search documentation

Search guides, the tutorial and every package.

On this page

Start from the template. src/app.service.ts and src/main.ts stay as they are.

Install#

Shell
bun add @rhythmjs/openapi @rhythmjs/scalar @rhythmjs/http

Describe the route#

apiOperation and apiResponse are ordinary middleware that carry documentation, so the router stays the single source of truth: what is documented is what runs.

TypeScript
// src/app.controller.ts
import { apiOperation } from "@rhythmjs/openapi/operation";
import { apiResponse } from "@rhythmjs/openapi/response";
import { RhythmRouter } from "@rhythmjs/router";
import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
import type { appService } from "./app.service";

export type AppContext = RhythmHttpContext & {
  appService: typeof appService;
};

export const appController = new RhythmRouter<AppContext>().get(
  "/",
  apiOperation({ summary: "Say hello", operationId: "getHello" }),
  apiResponse(200, { description: "A greeting", content: { "text/plain": { schema: { type: "string" } } } }),
  (ctx) => {
    ctx.response.headers.set("content-type", "text/plain");
    ctx.response.body = ctx.appService.getHello();
  },
);

Register the two modules#

openapiModule finds the router on its own and serves only the document, at /openapi.json. scalarModule is a separate module that serves the Scalar page at /docs, loading that document. Register both before the controller and before any catch-all.

TypeScript
// src/app.module.ts
import { defineDocument } from "@rhythmjs/openapi/document";
import { openapiModule } from "@rhythmjs/openapi/module";
import { Rhythm } from "@rhythmjs/rhythm";
import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
import { scalarModule } from "@rhythmjs/scalar";
import { appController } from "./app.controller";
import { appService } from "./app.service";

const openapiConfig = defineDocument({ info: { title: "Hello API", version: "1.0.0" } });

export const appModule = new Rhythm<RhythmHttpContext>({ name: "app", type: "module" })
  .provide(() => ({ appService }))
  .register(openapiModule.forRoot({ document: openapiConfig }))
  .register(scalarModule.forRoot())
  .use(appController.middleware())
  .use((ctx) => {
    ctx.response.status = 404;
    ctx.response.headers.set("content-type", "application/json");
    ctx.response.body = JSON.stringify({ success: false, status: 404, message: "Not Found" });
  });

Things to check#

  • GET /openapi.json is the generated document and GET /docs is the Scalar page. path and url on scalarModule.forRoot move the page and point it at another document URL.
  • Scalar loads from a CDN, so the browser needs network access to render the page.
  • Neither endpoint has authentication, and the document publishes your route map. Register them behind your own auth, or only outside production, if the API is not public.
  • The options of each package are on the OpenAPI page and in the @rhythmjs/scalar README.

The full app, with tests, is examples/scalar, built from the template.