openapi · finom — Claude Code skill: OpenAPI generation and installation
This skill shows how Vovk.ts derives an OpenAPI spec from procedures and validation schemas. Use it to document API operations and configure a docs UI such as Scalar.
npx skills add finom/vovk --skill openapi -a claude-codeWhat openapi does
The skill explains how Vovk.ts maps procedure fields to OpenAPI parameters, request bodies, and responses. Add operation metadata with @operation; @operation.error describes expected errors, while shared API details belong in vovk.config.mjs. Segments can have their own configuration.
You can access the spec from code and serve its JSON through a controller. The docs section covers Scalar, with Redoc and Swagger UI as alternatives. One constraint matters: a procedure without @operation, @operation.tool, or @operation.error is excluded from the spec, so each operation you want documented needs a decorator.
What it helps with
- generate an OpenAPI spec from procedures and validation schemas
- set shared API details and server URLs
- document expected endpoint errors
- configure separate specs for API segments
- serve the spec as JSON for a documentation page
Example prompts for the agent
- Configure OpenAPI generation for my Vovk.ts procedures and add metadata to the operations that should appear.
- Show me how to document endpoint errors with
@operation.error. - Set up a separate OpenAPI spec for the
adminsegment.
How to install openapi
1Skills CLI (Vercel)
npx skills add finom/vovk --skill openapi -a claude-code2Manually: put the files in place
~/.claude/skills/openapi/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/openapi/
1Skills CLI (Vercel)
npx skills add finom/vovk --skill openapi -a codex2Manually: put the files in place
.agents/skills/openapi/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add finom/vovk --skill openapi -a cursor2Manually: put the files in place
.cursor/skills/openapi/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install finom/vovk openapi2Manually: put the files in place
.github/skills/openapi/SKILL.md1Skills CLI (Vercel)
npx skills add finom/vovk --skill openapi -a gemini-cli2Platform CLI
gemini skills install https://github.com/finom/vovk --path skills/openapiManually: put the files in place
.devin/skills/openapi/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add finom/vovk --skill openapi -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about openapi
How do I install openapi in Claude Code?
Run npx skills add finom/vovk --skill openapi -a claude-code. The skill file is available at ~/.claude/skills/openapi/SKILL.md.
What does the openapi skill do?
It guides you through generating an OpenAPI spec from Vovk.ts procedures and validation schemas, documenting operations and errors, and configuring API docs.
Why is a procedure missing from the OpenAPI spec?
A procedure is included when it has @operation, @operation.tool, or @operation.error. Without one of these decorators, it is left out.
Source text
SKILL.md · MIT
---
name: openapi
description: Vovk.ts OpenAPI generation — how the OpenAPI 3.x spec is derived from procedures + validation schemas, the `@operation` decorator family (`@operation({...})` for summary/description/tags/security/deprecated, `@operation.error(status, message)` for error responses, `@operation.tool({...})` for AI tool metadata), top-level `outputConfig.openAPIObject` configuration (info, servers, license, tags, security, components), per-segment overrides, accessing the spec from code (`@/client/openapi`), serving the spec JSON from a controller, and rendering it with **Scalar** (the primary / recommended docs UI — picks up the TypeScript / Python / Rust code samples that Vovk generates via `x-codeSamples`) or alternatives like Redoc / Swagger UI. Use whenever the user asks for "API docs", "Scalar docs", "Swagger", "OpenAPI spec", "document my API", "generate OpenAPI from my controllers", "add an info/title/servers block", "segment-specific docs", "expose OpenAPI at /api/docs", "bearer auth in the spec", "document possible errors", "deprecate an endpoint". Does NOT cover procedure authoring → hand off to `procedure` skill. Does NOT cover AI tool derivation via `deriveTools` → hand off to `tools` skill (though `@operation.tool` is mentioned here since it's part of the decorator family). Does NOT cover third-party OpenAPI imports → hand off to `mixins` skill.
---
# Vovk.ts OpenAPI
OpenAPI 3.x spec is byproduct of code you already write. Procedures + validation schemas + `@operation` metadata → spec. No separate doc authoring.
## How procedures map to OpenAPI
| 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`; other statuses via `@operation({ responses: { 201: … } })`, merged over it |
| `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 family
Three 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`, `meta`. See **`tools` skill** for full usage. |
### `@operation({...})`
```ts
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 an operation decorator (`@operation`, `@operation.tool` or `@operation.error`), a procedure is left out of the spec, and `deriveTools` makes no tool of it. Add `@operation` to every procedure the spec or the tools should list.
### `@operation.error(status, message)`
Document errors your endpoint can throw so they show up in generated docs and client type narrowing:
```ts
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.vovk.body();
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:
```ts
@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. Any operation decorator makes a procedure a derived tool, with or without `summary`/`description`: the tool description is `x-tool.description`, else `summary` and `description`, else the method name. `@operation.tool({ hidden: true })` excludes it.
## Global OpenAPI object
Top-level fields (`info`, `servers`, `license`, `tags`, `security`) configured in `vovk.config.mjs`:
```ts
/** @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`.
## Per-segment overrides
Separate spec per segment with own `info` / `servers`:
```ts
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).
## Accessing the spec from code
Import path follows the client layout; same rules as generated RPC modules (see **`rpc` skill** for full picture).
### Composed (one merged spec)
```ts
import { openapi } from '@/client/openapi';
// Full OpenAPI 3.x object merged from all segments
```
This is what `vovk init` scaffolds. Composed client generates into `composedClient.outDir` (default `src/client`, or `client/` when the app isn't in `src/app`); `@/client` is the standard Next.js alias for that dir. Custom `outDir` shifts the path; projects without the alias use a relative import.
### Segmented (one spec per segment)
When `segmentedClient.enabled: true`, each segment writes its own `openapi.(ts|json)` under `<segmentedClient.outDir>/<segmentName>/`:
```ts
import { openapi as adminOpenAPI } from '@/client/admin/openapi';
import { openapi as rootOpenAPI } from '@/client/root/openapi'; // root segment folder is named "root"
```
If `segmentedClient.outDir` is `./sdk` the path becomes `@/sdk/admin/openapi`, etc. Per-segment spec files exist **only** under segmented client dirs; composed client emits just the merged `@/client/openapi`. Both clients enabled share the dir: composed files at the root, segments in subdirs.
Use these exports directly — no fetch required at runtime.
## Serving the spec at runtime
Cleanest Vovk-idiomatic way is plain controller method returning imported `openapi` object:
```ts
// src/modules/openapi/openapi-controller.ts
import { get, prefix, operation } from 'vovk';
import { openapi } from '@/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:
```ts
// src/app/api/openapi/route.ts
import { openapi } from '@/client/openapi';
export const GET = () => Response.json(openapi);
```
### Rendering it with a UI
**Scalar is the recommended renderer.** Vovk emits per-operation `x-codeSamples` (TypeScript via the generated TS 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:
```tsx
// 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** (`redoc-cli` or `<RedocStandalone>` component) — does read `x-codeSamples` (it's the Redocly convention Vovk emits). Works fine.
- **Swagger UI** (`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.
## Schema artifacts
`vovk dev` writes `.vovk-schema/` (one JSON file per segment, plus `_meta.json`); `vovk generate`, `vovk bundle`, the client, the OpenAPI document and AI tools read it. Commit it.
## Flows
### "Add title and version to my API docs"
```ts
// vovk.config.mjs
outputConfig: {
openAPIObject: {
info: { title: 'My API', version: '1.0.0' },
},
}
```
Run `npx vovk generate`. Done.
### "Describe every endpoint for Scalar"
Add `@operation({ summary, description, tags })` on every procedure. Run `vovk dev` so `.vovk-schema/` and the client pick it up. Scalar picks it up.
### "Expose API docs at /api/openapi"
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.
### "Bearer auth in the spec"
```ts
openAPIObject: {
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer' },
},
},
security: [{ bearerAuth: [] }],
}
```
Per-operation `security` lives on `@operation` if you want to override.
### "Admin API should have its own docs and server URL"
Per-segment override under `outputConfig.segments.admin.openAPIObject`. Import per-segment spec from the segmented client dir: `import { openapi } from '@/client/admin/openapi'` (path follows `segmentedClient.outDir`). Requires `segmentedClient.enabled: true`; composed client emits only the merged `@/client/openapi`.
### "Deprecate an endpoint"
```ts
@operation({ deprecated: true })
@get('old-endpoint')
static oldThing = procedure().handle(/* ... */);
```
### "Document the errors my endpoint throws"
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.
```ts
@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');
});
```
## Gotchas
- **A procedure without an operation decorator is left out**: no spec entry and no derived tool. `@operation({})` is enough to include it.
- **`@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.
- **Regeneration required**: after changing `@operation*` decorators, run `PORT=<port> npx vovk dev --next-dev --exit` (it rewrites `.vovk-schema/` and the client, then exits); `vovk generate` alone reads the committed `.vovk-schema/` and doesn't see decorator changes. A `vovk.config.mjs` change such as `outputConfig.openAPIObject` needs only `vovk generate`.
- **`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.
- **Segment key vs. path**: `outputConfig.segments.admin` matches `segmentName: 'admin'` — not folder path and not class name.
- **Content type is respected**: procedures with `contentType: 'multipart/form-data'` appear in spec with correct request-body encoding. Don't hand-patch generated spec.
- **No bundled docs UI**: Vovk exports JSON; wiring renderer is on you. **Scalar is the recommended pick** — renders per-language code samples Vovk emits, so endpoints show up with ready-to-copy TS / Python / Rust calls. Swagger UI / Redoc work fine but drop that part. Separation is deliberate; docs UIs evolve on their own release cadence.
© finom · MIT · finom/vovk
Similar skills
All skills for Backend and APIs →openapi by other authors comes first.