Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add finom/vovk --skill openapi -a claude-code
Open on GitHub

What 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 admin segment.

How to install openapi

1Skills CLI (Vercel)

npx skills add finom/vovk --skill openapi -a claude-code

2Manually: put the files in place

~/.claude/skills/openapi/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/openapi/

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.

From the same set15

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor