API Docs#

Auto-generate OpenAPI 3.0 documentation from your Zod validation schemas and serve a full-featured interactive API client.

Quick Start#

1. Enable docs in your app#

typescript
import { Buntok } from "@buntok/core";

const app = new Buntok();

app.apiDocs({
  path: "/docs",
  title: "My API",
  version: "1.0.0",
  description: "My awesome API documentation",
  safeOnProduction: true, // hides docs when NODE_ENV=production
});

app.listen(1212);
// Visit http://localhost:1212/docs
// swagger.json is auto-generated in the background on startup
Info
swagger.json is automatically generated in the background when the server starts. No need to run buntok make:docs manually. The generation runs via setImmediate() and does not affect API response time.
Info
Both /docs and /docs/ (with trailing slash) work. Asset paths (CSS/JS) are automatically resolved to the correct absolute paths, so the docs UI loads correctly regardless of how the URL is accessed.

Options#

OptionTypeDefaultDescription
pathstring"/docs"URL path for the docs UI
titlestring"API Documentation"API title shown in the docs UI and swagger.json
versionstring"1.0.0"API version shown in the docs UI and swagger.json
descriptionstringundefinedAPI description shown in the docs UI and swagger.json
safeOnProductionbooleanfalseWhen true, docs routes are not registered when NODE_ENV=production

Annotating Routes#

Use zValidator and zResponse in your route handlers. The CLI picks them up automatically.

Body Validation#

typescript

import { z } from "@buntok/core/middlewares/validator";

const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(["admin", "user"]).default("user"),
});

app.post(
  "/users",
  zValidator("body", CreateUserSchema),
  async (ctx) => {
    const data = ctx.valid("body");
    // ...
  }
);

Query & Params Validation#

typescript
// Query params
app.get(
  "/users",
  zValidator("query", z.object({
    page: z.coerce.number().int().default(1),
    limit: z.coerce.number().int().default(10),
    search: z.string().optional(),
  })),
  async (ctx) => {
    const { page, limit } = ctx.valid("query");
    // ...
  }
);

// Path params
app.get(
  "/users/:id",
  zValidator("params", z.object({
    id: z.string().uuid(),
  })),
  async (ctx) => {
    const { id } = ctx.valid("params");
    // ...
  }
);

Documenting Responses#

typescript

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  role: z.string(),
  createdAt: z.string(),
});

app.get(
  "/users/:id",
  zValidator("params", z.object({ id: z.string().uuid() })),
  zResponse(200, UserSchema, "User found"),
  zResponse(404, z.object({}), "User not found"),
  async (ctx) => {
    const user = await userRepo.findById(ctx.valid("params").id);
    if (!user) return ctx.error("Not found", 404);
    return ctx.json(user);
  }
);

Full Example#

typescript

import { Buntok } from "@buntok/core";
import { z } from "@buntok/core/middlewares/validator";

const app = new Buntok();

// ─── Schemas ───────────────────────────────────────
const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

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

// ─── Routes ────────────────────────────────────────
app.get(
  "/users",
  zValidator("query", z.object({
    page: z.coerce.number().default(1),
    limit: z.coerce.number().default(10),
  })),
  zResponse(200, z.array(UserSchema), "List of users"),
  async (ctx) => {
    const users = await userRepo.findAll();
    return ctx.json(users);
  }
);

app.post(
  "/users",
  zValidator("body", CreateUserSchema),
  zResponse(201, UserSchema, "User created"),
  async (ctx) => {
    const user = await userRepo.create(ctx.valid("body"));
    return ctx.json(user, 201);
  }
);

app.get(
  "/users/:id",
  zValidator("params", z.object({ id: z.string() })),
  zResponse(200, UserSchema, "User found"),
  async (ctx) => {
    const user = await userRepo.findById(ctx.valid("params").id);
    if (!user) return ctx.error("Not found", 404);
    return ctx.json(user);
  }
);

// ─── Enable API docs ───────────────────────────────
app.apiDocs({
  path: "/docs",
  title: "User Management API",
  version: "1.0.0",
  safeOnProduction: true,
});

app.listen(1212);

// Visit: http://localhost:1212/docs
// swagger.json is auto-generated in the background on startup

Production Safety#

Use safeOnProduction: true to automatically hide the docs UI when NODE_ENV=production. This prevents exposing your API documentation in production environments.

typescript
app.apiDocs({
  path: "/docs",
  title: "My API",
  safeOnProduction: true, // disabled when NODE_ENV=production
});
Warning
When safeOnProduction is true and NODE_ENV=production, the docs routes return 404. Make sure your deployment sets NODE_ENV=production.

Tips#

  • Routes without zValidator are still registered in the spec (no request/response schema)
  • If a schema type is unsupported, that route is skipped with a warning. Annotate with .openapi() from @asteasolutions/zod-to-openapi
  • The zResponse schema is wrapped in the standard Buntok envelope: { success, message, data }
  • The interactive docs UI includes request testing, code snippet generation, cookie management, and schema-powered body previews
Info
swagger.json is auto-generated when you call app.listen(). For manual regeneration (e.g., CI/CD), run buntok make:docs - it imports your src/index.ts and generates the file.