Skip to content

Declarative vs Chained

to-openapi provides two APIs that produce identical output: the openapi() function and the OpenAPI class. This page helps you choose the right one and shows how to use each effectively.

openapi() — Declarative

Define your entire API in a single object. All routes, schemas, and metadata go in one place.

ts
import { openapi } from "to-openapi";
import { z } from "zod";

const PetSchema = z.object({
  id: z.number(),
  name: z.string(),
});

const ErrorSchema = z.object({
  message: z.string(),
  code: z.string(),
});

const doc = openapi({
  info: { title: "Pet Store", version: "1.0.0" },
  schemas: {
    Pet: PetSchema,
    Error: ErrorSchema,
  },
  paths: {
    "GET /pets": {
      summary: "List all pets",
      tags: ["Pets"],
      200: PetSchema,
    },
    "POST /pets": {
      summary: "Create a pet",
      tags: ["Pets"],
      body: PetSchema,
      201: PetSchema,
      400: ErrorSchema,
    },
    "GET /pets/{id}": {
      summary: "Get a pet",
      tags: ["Pets"],
      200: PetSchema,
      404: ErrorSchema,
    },
  },
});

Route keys combine the HTTP method and path in a single string: "GET /pets", "POST /pets/{id}". This keeps route definitions compact and scannable.

OpenAPI — Builder

Build your API incrementally with method calls. Routes and schemas are registered one at a time.

ts
import { OpenAPI } from "to-openapi";
import { z } from "zod";

const PetSchema = z.object({
  id: z.number(),
  name: z.string(),
});

const ErrorSchema = z.object({
  message: z.string(),
  code: z.string(),
});

const api = new OpenAPI({
  info: { title: "Pet Store", version: "1.0.0" },
});

api.schema("Pet", PetSchema);
api.schema("Error", ErrorSchema);

api.route("get", "/pets", {
  summary: "List all pets",
  tags: ["Pets"],
  200: PetSchema,
});

api.route("post", "/pets", {
  summary: "Create a pet",
  tags: ["Pets"],
  body: PetSchema,
  201: PetSchema,
  400: ErrorSchema,
});

api.route("get", "/pets/{id}", {
  summary: "Get a pet",
  tags: ["Pets"],
  200: PetSchema,
  404: ErrorSchema,
});

const doc = api.document();

All methods return this, so you can chain them:

ts
const doc = new OpenAPI({ info: { title: "Pet Store", version: "1.0.0" } })
  .schema("Pet", PetSchema)
  .schema("Error", ErrorSchema)
  .route("get", "/pets", { 200: PetSchema })
  .route("post", "/pets", { body: PetSchema, 201: PetSchema })
  .document();

When to Use Which

ScenarioRecommended
All routes known at startupopenapi()
Routes registered across multiple files or modulesOpenAPI class
Config-driven or static specsopenapi()
Framework middleware that registers routes dynamicallyOpenAPI class
Quick prototypingopenapi()
Large API with routes grouped by domainOpenAPI class

Use openapi() when routes are defined in one place

The declarative style works well when your entire API is visible in a single file. You get a complete picture of every route and schema at a glance.

Use OpenAPI when routes are spread across modules

The builder style shines when different parts of your codebase register their own routes. Pass the OpenAPI instance to each module and let them call .route() independently:

ts
// pets.ts
export function registerPetRoutes(api: OpenAPI) {
  api.route("get", "/pets", { 200: PetSchema });
  api.route("post", "/pets", { body: CreatePetSchema, 201: PetSchema });
}

// users.ts
export function registerUserRoutes(api: OpenAPI) {
  api.route("get", "/users", { 200: UserSchema });
  api.route("post", "/users", { body: CreateUserSchema, 201: UserSchema });
}

// main.ts
const api = new OpenAPI({ info: { title: "API", version: "1.0.0" } });
registerPetRoutes(api);
registerUserRoutes(api);
const doc = api.document();

Same Output

Both APIs use the same internal pipeline: route shorthand expansion, schema resolution, $ref deduplication, plugin hooks, and document assembly. The output is identical given the same inputs.

ts
import { openapi, OpenAPI } from "to-openapi";

// These produce the same document
const a = openapi({
  info: { title: "API", version: "1.0.0" },
  schemas: { Pet: PetSchema },
  paths: { "GET /pets": { 200: PetSchema } },
});

const b = new OpenAPI({ info: { title: "API", version: "1.0.0" } })
  .schema("Pet", PetSchema)
  .route("get", "/pets", { 200: PetSchema })
  .document();

Same Route Shorthand

Both APIs accept the same route shorthand format for defining operations. Request parameters, bodies, responses, tags, security, and vendor extensions work identically regardless of which API you use.