From vovk
Generates OpenAPI 3.x specs from Vovk.ts procedures, validation schemas, and @operation decorators. Configures global/per-segment metadata, serves JSON, renders with Scalar/Redoc/Swagger UI.
npx claudepluginhub finom/vovkThis skill uses the workspace's default tool permissions.
OpenAPI 3.x spec is byproduct of code you already write. Procedures + validation schemas + `@operation` metadata → spec. No separate doc authoring.
Generates OpenAPI specs and interactive API docs with Swagger/Redoc. Handles spec-first contracts and code-first auto-generation from Express, FastAPI, NestJS, Spring Boot.
Generates OpenAPI 3.0+ specs from existing API code, enhances with schemas/examples/errors, creates interactive docs/SDKs, and validates compliance.
Generates and maintains OpenAPI 3.1 specifications from code, design-first specs, and validation patterns. Use for API documentation, SDK generation, and contract compliance.
Share bugs, ideas, or general feedback.
OpenAPI 3.x spec is byproduct of code you already write. Procedures + validation schemas + @operation metadata → spec. No separate doc authoring.
Covers:
@operation({...}) — summary, description, tags, security, deprecated, any OpenAPI 3.1 OperationObject field.@operation.error(status, message) — document known error responses.@operation.tool({...}) — AI-tool metadata (x-tool). Read by deriveTools; full usage in tools skill.outputConfig.openAPIObject).outputConfig.segments.<name>.openAPIObject).import { openapi } from 'vovk-client/openapi'.Out of scope:
procedure skill.deriveTools() / createTool() / MCP wiring → tools skill.mixins skill.| Procedure field | OpenAPI output |
|---|---|
params schema | parameters with in: 'path' |
query schema | parameters with in: 'query' |
body schema | requestBody (content type from contentType, default application/json) |
output schema | responses.200 with application/json |
iteration schema | responses.200 with application/jsonl |
@operation fields | operation-level metadata (summary, description, tags, etc.) |
Anything expressible in procedure() call — request shape, response shape, content type — lands in spec automatically.
@operation decorator familyThree decorators, same namespace, all attached to operation:
| Decorator | Writes to | Purpose |
|---|---|---|
@operation({...}) | operationObject | Standard OpenAPI metadata (summary, description, tags, deprecated, security, …). Accepts any field from OpenAPI 3.1's OperationObject. |
@operation.error(status, message) | operationObject.responses[status] | Document a known error shape. Generates response referencing #/components/schemas/VovkErrorResponse with message pinned to a literal. |
@operation.tool({...}) | operationObject['x-tool'] | AI-tool metadata read by deriveTools — name, description, title, hidden. See tools skill for full usage. |
@operation({...})import { operation, put, procedure } from 'vovk';
import { z } from 'zod';
@operation({
summary: 'Update user',
description: 'Update user profile by ID.',
tags: ['users'],
// plus any other OpenAPI OperationObject field:
// deprecated, externalDocs, security, ...
})
@put('{id}')
static updateUser = procedure({
params: z.object({ id: z.string().uuid() }),
body: z.object({ email: z.string().email() }),
output: z.object({ success: z.boolean() }),
}).handle(async (req) => { /* ... */ });
Without @operation, procedure still appears in spec — minus summary/description. LLM tool callers and doc readers both benefit, so add it on every user-facing procedure.
@operation.error(status, message)Document errors your endpoint can throw so they show up in generated docs and client type narrowing:
import { operation, post, procedure, HttpStatus, HttpException } from 'vovk';
@operation({ summary: 'Create user' })
@operation.error(HttpStatus.BAD_REQUEST, 'Email is already taken')
@operation.error(HttpStatus.BAD_REQUEST, 'Invalid email format')
@operation.error(HttpStatus.NOT_FOUND, 'Organization not found')
@post()
static createUser = procedure({
body: z.object({ email: z.string(), orgId: z.string() }),
}).handle(async (req) => {
const { email, orgId } = await req.json();
if (!await orgExists(orgId)) throw new HttpException(HttpStatus.NOT_FOUND, 'Organization not found');
if (await emailTaken(email)) throw new HttpException(HttpStatus.BAD_REQUEST, 'Email is already taken');
// ...
});
Same status can be declared multiple times with different messages — they merge into enum of allowed messages under that status code. Throw HttpException(status, message) with message from declared set.
@operation.tool({...})Brief shape; tools skill covers derivation. Sets x-tool on operation:
@operation.tool({
name: 'get_user_by_id',
title: 'Get user by ID',
description: 'Retrieves a user by their unique ID.',
// hidden: true to exclude from derived tools
})
@operation({ summary: 'Get user by ID' })
@get('{id}')
static getUser = /* ... */;
Mentioned here only because part of operation.* namespace. Procedures without summary or description (from @operation) are excluded from derived tools by default — @operation.tool block overrides that.
Top-level fields (info, servers, license, tags, security) configured in vovk.config.mjs:
/** @type {import('vovk').VovkConfig} */
const config = {
outputConfig: {
openAPIObject: {
info: {
title: 'My App API',
description: 'API for my app.',
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
version: '1.0.0',
},
servers: [
{ url: 'https://myapp.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Local' },
],
tags: [
{ name: 'users', description: 'User management' },
],
security: [{ bearerAuth: [] }],
},
},
};
export default config;
Put shared metadata here. Per-operation details stay on @operation.
Separate spec per segment with own info / servers:
const config = {
outputConfig: {
segments: {
admin: {
openAPIObject: {
info: { title: 'Admin API', version: '1.0.0' },
servers: [{ url: 'https://admin.example.com' }],
},
},
},
},
};
Key (admin) is segmentName from initSegment (root segment uses ""). Each segment's spec is served and exported independently — useful when segments have different audiences (public SDK vs internal admin).
Import path depends on which client layout the project uses — same matrix as generated RPC modules (see rpc skill for full picture).
vovk-client barrelimport { openapi } from 'vovk-client/openapi';
// Full OpenAPI 3.x object merged from all segments
This is what vovk init scaffolds. The vovk-client npm package re-exports generated .vovk-client/openapi.js file; nothing lands inside source tree.
outDir aliasIf project uses TypeScript client template and emits into the repo (e.g. composedClient.outDir: './src/client'), vovk-client barrel isn't involved at all. Import from whatever alias your tsconfig paths resolves that directory to:
import { openapi } from '@/client/openapi';
When segmentedClient.enabled: true, each segment writes its own openapi.(ts|js|json) under <segmentedClient.outDir>/<segmentName>/. Import from alias — vovk-client barrel is never the entry point here:
import { openapi as adminOpenAPI } from '@/client/admin/openapi';
import { openapi as rootOpenAPI } from '@/client/openapi'; // root segment lives at the outDir root
If segmentedClient.outDir is ./sdk the path becomes @/sdk/admin/openapi, etc. There is no vovk-client/admin/openapi or vovk-client/<segmentName>/openapi — that path does not exist.
Use these exports directly — no fetch required at runtime.
Cleanest Vovk-idiomatic way is plain controller method returning imported openapi object:
// src/modules/openapi/OpenApiController.ts
import { get, prefix, operation } from 'vovk';
import { openapi } from 'vovk-client/openapi';
@prefix('openapi')
export default class OpenApiController {
@get()
@operation({ summary: 'OpenAPI spec', tags: ['meta'] })
static getOpenAPI() {
return openapi; // return the spec as-is, not { openapi }
}
}
Register in segment's route.ts like any other controller. JSON now reachable at GET /api/openapi — any docs viewer (Scalar, Redoc, Swagger UI) can point at it.
For just raw JSON without going through controller, Next.js route handler works too:
// src/app/api/openapi/route.ts
import { openapi } from 'vovk-client/openapi';
export const GET = () => Response.json(openapi);
Scalar is the recommended renderer. Vovk emits per-operation x-codeSamples (TypeScript via vovk-client, Python via vovk-python, Rust via vovk-rust), and Scalar renders them inline next to each endpoint — resulting docs look like real SDK reference, not stripped-down Swagger page.
Install @scalar/api-reference-react and point at URL exposed above:
// src/app/api-docs/page.tsx
'use client';
import { ApiReferenceReact } from '@scalar/api-reference-react';
import '@scalar/api-reference-react/style.css';
export default function ApiDocs() {
return (
<ApiReferenceReact
configuration={{
url: '/api/openapi', // or '/api/openapi.json'
}}
/>
);
}
For non-React page just drop in @scalar/api-reference CDN bundle and pass same URL — same story, different transport.
Alternatives exist but don't surface x-codeSamples as prominently:
redoc-cli or <RedocStandalone> component) — does read x-codeSamples (it's the Redocly convention Vovk emits). Works fine.swagger-ui-react, spec={openapi}) — works, but no multi-language request examples.All three need just spec JSON — endpoint above is the integration point. Check renderer's current docs for exact prop name; those APIs change faster than Vovk does.
Each segment emits one JSON file to .vovk-schema/:
.vovk-schema/
root.json
admin.json
foo/bar.json
_meta.json
npx vovk generate produces these; client, OpenAPI, and AI tool pipelines all read from them. Commit .vovk-schema/ so builds are reproducible.
// vovk.config.mjs
outputConfig: {
openAPIObject: {
info: { title: 'My API', version: '1.0.0' },
},
}
Run npx vovk generate. Done.
Add @operation({ summary, description, tags }) on every procedure. Regenerate. Scalar picks it up.
Mount OpenApiController returning openapi (snippet above), then point Scalar at that URL — recommended renderer because surfaces Vovk's per-language code samples (TS / Python / Rust) inline. Redoc works too; Swagger UI works but skips code-sample block.
openAPIObject: {
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer' },
},
},
security: [{ bearerAuth: [] }],
}
Per-operation security lives on @operation if you want to override.
Per-segment override under outputConfig.segments.admin.openAPIObject. Import per-segment spec from segmented client's local alias — import { openapi } from '@/client/admin/openapi' (path follows segmentedClient.outDir). There is no vovk-client/admin/openapi barrel.
@operation({ deprecated: true })
@get('old-endpoint')
static oldThing = procedure().handle(/* ... */);
Stack @operation.error(status, message) per error case. Keep message string identical between decorator and HttpException you throw — spec pins each status's message field to enum of declared values.
@operation.error(HttpStatus.NOT_FOUND, 'User not found')
@operation.error(HttpStatus.CONFLICT, 'Email already exists')
@post()
static createUser = procedure({ body: /* ... */ }).handle(async (req) => {
// throw new HttpException(HttpStatus.NOT_FOUND, 'User not found');
});
experimentalDecorators — how do I set operation metadata?"Use decorate() (see procedure skill for full pattern):
import { decorate, post, operation, procedure, HttpStatus } from 'vovk';
static createUser = decorate(
post(),
operation({ summary: 'Create user', tags: ['users'] }),
operation.error(HttpStatus.CONFLICT, 'Email already exists'),
procedure({ body: /* ... */ })
).handle(async (req) => { /* ... */ });
@operation is advisory: omitting it doesn't break spec, but descriptions/tags go missing. LLM tool callers and doc readers both suffer.@operation.error message strings are identity-linked to HttpException: decorator pins response's message to enum of declared values. If runtime throws different message string, docs will be accurate about status code but wrong about message. Keep them in sync, or parameterize via shared constant.@operation* or changing vovk.config.mjs, run vovk generate (or vovk dev).x-tool doesn't affect OpenAPI semantics — consumed only by deriveTools. Safe to include; x- fields are standard OpenAPI extension mechanism, though some strict linters still warn.outputConfig.segments.admin matches segmentName: 'admin' — not folder path and not class name.contentType: 'multipart/form-data' appear in spec with correct request-body encoding. Don't hand-patch generated spec.