Overview
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
$ npm install @vercube/schema
$ bun install @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
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:
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).
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:
| URL | Content |
|---|---|
/_schema/ | OpenAPI 3.0 JSON |
/_schema/docs | Scalar API Reference (HTML) |
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.