Overview

OpenAPI generation and interactive API docs for Vercube

The Schema module (@vercube/schema) turns your route metadata and Zod validation schemas into an OpenAPI 3 document at runtime. It also serves a Scalar API Reference UI so you can browse and try your API without extra tooling.

Define validation once with @Body / @QueryParams, annotate routes with @Schema, and get:

  • OpenAPI JSON at GET /_schema/
  • Scalar docs at GET /_schema/docs (enabled by default)

Installation

$ pnpm add @vercube/schema

The package re-exports z with OpenAPI helpers from @asteasolutions/zod-to-openapi. Import z from @vercube/schema (not only from zod) when you want .openapi() metadata on your schemas.

Quick Start

Register the plugin

src/Boot/Setup.ts
import { createApp } from '@vercube/core';
import { SchemaPlugin } from '@vercube/schema';

export async function setup(app: App) {
  app.addPlugin(SchemaPlugin);
}

Or from vercube.config.ts with a class plugin:

vercube.config.ts
import { defineConfig, withPluginOptions } from '@vercube/core';
import { SchemaPlugin } from '@vercube/schema';

export default defineConfig({
  plugins: [SchemaPlugin],
});

Annotate a route

Use @Schema on controller methods. The decorator registers the route in the OpenAPI registry. Request body and query schemas are picked up automatically when you use validationSchema on @Body and @QueryParams (see Decorators).

src/controllers/UserController.ts
import { Body, Controller, Post, QueryParams } from '@vercube/core';
import { Schema, z } from '@vercube/schema';

const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const ListUsersQuerySchema = z.object({
  page: z.coerce.number().optional(),
});

@Controller('/users')
export class UserController {
  @Post('/')
  @Schema({
    summary: 'Create user',
    responses: {
      201: {
        description: 'Created user',
        content: {
          'application/json': {
            schema: z.object({ id: z.string(), name: z.string() }),
          },
        },
      },
    },
  })
  create(
    @Body({ validationSchema: CreateUserSchema }) body: z.infer<typeof CreateUserSchema>,
    @QueryParams({ validationSchema: ListUsersQuerySchema }) _query: z.infer<typeof ListUsersQuerySchema>,
  ) {
    return { id: '1', ...body };
  }
}

Open the docs

With the dev server running:

URLContent
/_schema/OpenAPI 3.0 JSON
/_schema/docsScalar API Reference (HTML)
The playground app already registers SchemaPlugin - try http://localhost:3000/_schema/docs after pnpm dev in playground/.

Zod and OpenAPI metadata

Use .openapi() on Zod types for richer docs (examples, component names):

const UserSchema = z
  .object({
    id: z.string().openapi({ example: 'usr_123' }),
    name: z.string().openapi({ example: 'Jane Doe' }),
  })
  .openapi('User');

Reference named components in @Schema responses:

@Schema({
  responses: {
    200: {
      description: 'User',
      content: {
        'application/json': { schema: UserSchema },
      },
    },
  },
})

Plugin options

SchemaPlugin accepts optional SchemaPluginOptions:

import { SchemaPlugin } from '@vercube/schema';

app.addPlugin(SchemaPlugin, {
  scalar: {
    pageTitle: 'My API',
    openApiUrl: '/_schema/',
    config: { theme: 'purple' },
  },
});

// Disable Scalar UI (OpenAPI JSON remains at /_schema/):
app.addPlugin(SchemaPlugin, { scalar: false });

See Scalar integration for all Scalar-related options.

Relationship to validation

Schema generation is additive to Validation. Validation runs on every request via middleware; @Schema only affects documentation and the generated OpenAPI spec. Use the same Zod schemas for both when possible so docs stay in sync with runtime behavior.

Previous

Decorators

OpenAPI metadata with the @Schema decorator

Next