Skip to Content
OpenAPI Mixins

Code Generation via OpenAPI Mixins

Vovk.ts can add modules generated from one or more OpenAPI specifications to its client. Use it to call third-party APIs from a Next.js/Vovk.ts app, or as a standalone code generator that doesn’t need Next.js. This page covers the configuration options for code generation. The generate command doesn’t need a config file.

Features

Call Signature

Every method has the same call signature, a single argument object:

import { PetstoreAPI } from '@/client'; await PetstoreAPI.updatePetWithForm({ params: { petId: 1 }, // URL params (if any) query: { name: 'Doggo', status: 'sold' }, // Query params (if any) init: { headers: { 'X-Custom-Header': 'value' } }, // Optional: fetch init apiRoot: 'https://api.example.com', // Optional: override API root URL }); await PetstoreAPI.updatePet({ body: { id: 1, name: 'Doggo', photoUrls: [] }, // Request body (if any) disableClientValidation: true, // Optional: disable client-side validation });

The client doesn’t type or send the header and cookie parameters of the OpenAPI document. Send them by hand with init.headers (TypeScript), headers= (Python) or headers (Rust); a cookie goes in a Cookie header.

withDefaults creates a copy of an API module with default options:

import { PetstoreAPI } from '@/client'; const PetstoreAPIWithAuth = PetstoreAPI.withDefaults({ init: { headers: { Authorization: 'Bearer my-token', }, }, apiRoot: 'https://api.example.com', }); await PetstoreAPIWithAuth.updatePet({ body: { name: 'Doggo', photoUrls: [] }, });

Query and Form Styles

A method sends each query parameter in the style and explode that the OpenAPI document declares for it: form (repeated keys, or comma-separated with explode: false), spaceDelimited, pipeDelimited or deepObject (filter[status]=sold). A parameter that declares neither is sent in OpenAPI’s default, form exploded: tags=a&tags=b for an array, and the keys of an object as parameters of their own.

// tags declared with explode: true, ids with explode: false await PetstoreAPI.listPets({ query: { tags: ['a', 'b'], ids: [1, 2] } }); // GET /pets?tags=a&tags=b&ids=1,2

In an application/x-www-form-urlencoded body, a property whose encoding declares a style is sent in that style, as Stripe’s metadata with deepObject: metadata[order_id]=6735. Other properties are sent as in any form body: an array as repeated keys, an object as JSON.

Client-Side Validation and Schema Availability

Generated API modules can validate the input with Ajv  on the client, before they send the request. To turn validation off, pass disableClientValidation: true.

import { UserAPI } from '@/client'; await UserAPI.updateUser({ // ...throws a validation error if the input is invalid });

The generated code also exports the Vovk.ts schema. The composed client and each chunk of the segmented client export it as a schema object.

import { schema } from '@/client'; // import { schema } from '@/client/schema';

Every generated method has its schema too:

import { UserAPI } from '@/client'; UserAPI.updateUser.schema.validation?.body; // JSON Schema for request body

Deriving AI Tools

Every generated API module can be turned into AI tools for function calling APIs.

import { deriveTools } from 'vovk'; import { PetstoreAPI } from '@/client'; const tools = deriveTools({ modules: { PetstoreAPI, }, }); console.log(tools); // [{ execute: (llmInput) => {}, name: 'PetstoreAPI_updatePet', description: 'Update an existing pet by Id', inputSchema: { ... } }, ...]

Python and Rust Clients

Vovk.ts templates can also generate Python and Rust clients, with client-side validation and the same options. See the Python and Rust pages.

Type Inference for Unnamed Schemas

In good OpenAPI design, components/schemas defines the input and output data, so the generated client functions get named types. Not every specification does this, and moving every input and output into components/schemas can be impractical.

Without components/schemas, many code generators produce awkward type names, such as ApiUsersIdPostRequest or ApiUsersIdPost200Response. Developers then often use the Parameters<T>[index] generic, or skip code generation and use fetch or axios with manual casts.

Vovk.ts infers types for unnamed schemas. When a specification doesn’t define components/schemas, these type utilities give you the input and output types:

import { PetstoreAPI } from '@/client'; import type { VovkBody, VovkQuery, VovkParams, VovkOutput } from 'vovk'; type Body = VovkBody<typeof PetstoreAPI.updatePet>; type Query = VovkQuery<typeof PetstoreAPI.updatePet>; type Params = VovkParams<typeof PetstoreAPI.updatePet>; type Output = VovkOutput<typeof PetstoreAPI.updatePet>;

In the Python client, the types are TypedDicts:

from vovk_client import PetstoreAPI body: PetstoreAPI.UpdatePetBody = {"id": 1, "name": "Doggo", "photoUrls": []} output: PetstoreAPI.UpdatePetOutput = {"id": 1, "name": "Doggo", "photoUrls": [], "status": "sold"}

In the Rust client, the types are nested modules with structs and enums. They follow the structure of the specification’s schemas, with the _:: separator:

use vovk_client::petstore_api::update_pet_::{ body as Body, output as Output, Pet, Pet_::status as Status, // for nested data };

Bundle

Once you configure the bundle.build function (see bundle page), the bundle command builds the TypeScript client into an npm package. It also writes package.json and README.md, and the README shows each method with a code sample. See the “Hello World” example for details.

A bundle needs package.json and tsconfig.json at the project root.

Getting Started

Using Standalone Codegen

To use the code generator as a standalone CLI, even without package.json, install vovk-cli globally or as a dev dependency. Skip this section if you use Vovk.ts in a Next.js project.

npm install -g vovk-cli

Or install vovk-cli as a dev dependency and vovk, openapi3-ts and vovk-ajv as regular dependencies:

npm install -D vovk-cli
npm install vovk openapi3-ts vovk-ajv

The composed client, which combines all generated API clients into one, goes to client/ in standalone codegen, or to src/client in a Next.js app whose app folder is src/app (composedClient.outDir changes it). Your code imports it directly, for example as @/client.

Create Config File

To customize the generated code, create a config file as the config page describes, or run vovk-cli init:

npx vovk-cli init

A basic config file looks like this:

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { imports: { validateOnClient: 'vovk-ajv', }, }, }; export default config;

Define OpenAPI mixins

A mixin is a pseudo-segment in outputConfig.segments with an openAPIMixin property. The key is the mixin name. Generation fails for root, or for a name that a segment or another mixin has, ignoring case, because the segmented client writes each one to a folder of that name. Two mixins also can’t give the same Mixins namespace, like my-api and myApi, and two segments of the composed client can’t export modules of the same name. The property accepts:

  • source: an object with url (a remote spec), file (a local spec) or object (an inline spec). With url, you can add a fallback file path. The CLI reads it when the fetch fails, and updates it after each successful fetch.
  • getModuleName: a string or a function that names the generated API modules. A string is a fixed module name and must be a valid identifier. The default is 'api'.
  • getMethodName: a string or a function that names the methods. The strings are camel-case-operation-id and auto (the default). camel-case-operation-id converts an operationId such as get_users to getUsers, with a leading underscore when it starts with a digit (2fa_verify becomes _2FaVerify). With it, generation fails for an operation without an operationId. auto uses the operationId, or the HTTP method and path when the operationId is unsuitable or missing. From the path, auto names GET /users listUsers, GET /users/{id} getUsersById and PATCH /users/{userId}/profile patchUsersProfileByUserId. When two operations get the same name, the later one in the document gets a _2 suffix, and the CLI warns.
  • apiRoot (optional): the API root URL. The apiRoot call option overrides it. Required if the OAS document has no servers property.
  • filterOperations (optional): a predicate that keeps only the operations it returns true for. See Filter Operations and Prune Components.
  • pruneComponents (optional, default false): removes the components that the kept operations don’t reference from the generated schema. See Filter Operations and Prune Components.

Petstore example with a remote URL and a local fallback:

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { imports: { validateOnClient: 'vovk-ajv', }, segments: { petstore: { openAPIMixin: { source: { url: 'https://petstore3.swagger.io/api/v3/openapi.json', fallback: './.openapi-cache/petstore.json', }, getModuleName: 'PetstoreAPI', getMethodName: 'auto', apiRoot: 'https://petstore3.swagger.io/api/v3', }, }, }, }, }; export default config;

This generates one PetstoreAPI module, with a method for each operation in the spec.

import { PetstoreAPI } from '@/client'; await PetstoreAPI.findPetsByStatus({ query: { status: 'available' } });

When getModuleName or getMethodName is a function, it receives:

  • operationObject: the Operation Object for the operation.
  • method: the HTTP method (uppercase string).
  • path: the operation path.
  • openAPIObject: the entire OpenAPI document.

A larger example is the GitHub REST API . Each operationId in the GitHub OpenAPI spec  has the form scope/operation, such as repos/remove-status-check-contexts or codespaces/list-for-authenticated-user. The config below makes module names from the first part and method names from the second, with lodash.

For example, issues/list-for-org becomes the GithubIssuesAPI module with a listForOrg method.

vovk.config.js
// @ts-check import camelCase from 'lodash/camelCase.js'; import startCase from 'lodash/startCase.js'; /** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { imports: { validateOnClient: 'vovk-ajv', }, segments: { github: { openAPIMixin: { source: { url: 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json', fallback: './.openapi-cache/github.json', }, getModuleName: ({ operationObject }) => { const [operationNs] = operationObject.operationId?.split('/') ?? ['unknown']; return `Github${startCase(camelCase(operationNs)).replace(/ /g, '')}API`; }, getMethodName: ({ operationObject }) => { const [, operationName] = operationObject.operationId?.split('/') ?? ['', 'ERROR']; return camelCase(operationName); }, }, }, }, }, }; export default config;

The result is several modules:

import { GithubIssuesAPI, GithubReposAPI, GithubPullsAPI, GithubActionsAPI, GithubUsersAPI } from '@/client'; await GithubIssuesAPI.listForOrg({ params: { org: 'octocat' } }); await GithubReposAPI.removeStatusCheckContexts({ params: { owner: 'octocat', repo: 'Hello-World', branch: 'main' }, body: { contexts: ['ci/build'] }, }); await GithubPullsAPI.list({ params: { owner: 'octocat', repo: 'Hello-World' } }); await GithubActionsAPI.listRepoWorkflows({ params: { owner: 'octocat', repo: 'Hello-World' } }); await GithubReposAPI.getLatestRelease({ params: { owner: 'octocat', repo: 'Hello-World' } }); await GithubUsersAPI.getAuthenticated();

Third-party OAS documents can contain keywords that JSON Schema doesn’t define, such as example, discriminator or x-*. vovk-ajv ignores them, and leaves a schema that Ajv can’t compile for the server to validate.

Filter Operations and Prune Components

A large spec makes a large client: every operation becomes a method, and the generated schema carries the whole components dictionary. Stripe’s spec gives 559 methods and 1823 components, megabytes of generated JSON for an app that may call a few endpoints. Two options cut this down:

  • filterOperations: only the operations the predicate returns true for are generated. It receives the same object as the naming functions (operationObject, method, path, openAPIObject) and runs before them, so a filtered-out operation produces no method, types or validation schemas. Omit it to keep all operations.
  • pruneComponents (default false): removes from the segment schema every component that the kept operations don’t reference, directly or through other components. Don’t turn it on if you import Mixins.<Segment>.<Component> types for components that no kept operation uses: they are removed with their schemas.
vovk.config.js
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { segments: { stripe: { openAPIMixin: { source: { url: 'https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.sdk.json', fallback: './.openapi-cache/stripe.json', }, apiRoot: 'https://api.stripe.com', getModuleName: 'StripeAPI', getMethodName: 'camel-case-operation-id', filterOperations: ({ method, path }) => method === 'GET' && /^\/v1\/(invoices|customers|subscriptions|charges|refunds|disputes|balance_transactions|prices)$/.test(path), pruneComponents: true, }, }, }, }, }; export default config;

For this Stripe subset, the generated mixins.json drops from ~19 MB (559 operations, 1823 components) to ~2.4 MB (8 operations, 927 components); minified, from ~9.6 MB to ~1.4 MB. Stripe’s core objects reference each other a lot, so the 8 operations still reach 927 components. Measured on Stripe’s spec 2026-09-30.endive; the numbers change as the spec grows.

Customize Fetcher

Each mixin can have its own fetch function, or all mixins can share one. The fetcher sets authorization headers, runs client-side validation, and sends and handles the HTTP requests.

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { // ... segments: { petstore: { openAPIMixin: { /* ... */ }, imports: { fetcher: './src/lib/petstore-fetcher' }, }, }, }, }; export default config;

Composed Client

By default, the composed client uses the ts template to generate a TypeScript client.

import { PetstoreAPI, GithubIssuesAPI } from '@/client'; await PetstoreAPI.findPetsByStatus({ query: { status: 'available' } }); await GithubIssuesAPI.listForOrg({ params: { org: 'finom' } });

The Mixins namespace holds the types generated from components/schemas of all mixed-in OpenAPI specifications, as an alternative to inference. Each mixin gets a namespace, and each component a type, both named in PascalCase: the petstore mixin’s Pet component is Mixins.Petstore.Pet. Letters of any script are kept. A name that would start with a digit gets a leading underscore: 2FAConfig becomes _2FaConfig. When two components get the same name, the later one in the document takes a number: user-profile and UserProfile become UserProfile and UserProfile2.

import type { VovkOutput } from 'vovk'; import { PetstoreAPI, type Mixins } from '@/client'; const pet: Mixins.Petstore.Pet = { id: 1, name: 'Doggo', photoUrls: [] }; // Alternatively: const pet2: VovkOutput<typeof PetstoreAPI.getPetById> = { id: 1, name: 'Doggo', photoUrls: [] };

composedClient.outDir changes the output folder:

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { composedClient: { outDir: './src/lib/client', // a custom folder }, }; export default config;
import { PetstoreAPI, GithubIssuesAPI } from '@/lib/client'; // ...

Segmented Client

The segmented client splits the code into chunks. Each mixin goes to a folder named after its segment (petstore, github and so on, from outputConfig.segments).

By default, the output goes to the composed client’s folder: src/client with a src/app folder, client/ otherwise. segmentedClient.outDir changes the folder.

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { segmentedClient: { enabled: true, outDir: './src/lib/client', // a folder in your code prettifyClient: true, // prettify the output }, }; export default config;
import { PetstoreAPI } from '@/lib/client/petstore'; import { GithubIssuesAPI } from '@/lib/client/github'; // ...
Last updated on