Skip to Content
OpenAPI Codegen

Code generation via OpenAPI mixins

Vovk.ts is able to mix the existing Vovk.ts client with modules generated from one or more OpenAPI specifications. This allows to easily integrate third-party APIs into Next.js/Vovk.ts application, or to be used as a standalone Codegen tool as it doesn’t require Next.js to be installed. This page will cover configuration file options related to code generation, but it’s good to know that generate command doesn’t require config file to be present.

Note

The generated exports are still refered as “RPC modules” for consistency, not being actual “RPC” as we don’t know if a client-side method actually invokes same-named server-side procedure.

Features

Сomprehensible syntax

Unlike other OpenAPI code generators, Vovk.ts always preserves the same set of options for each method in a single argument object, making it easy to remember and use:

import { PetstoreRPC } from 'vovk-client'; await PetstoreRPC.updatePet({ params: { id: '123' }, // URL params if any query: { hello: 'world' }, // Query params if any body: { name: 'Doggo' }, // Request body if any disableClientValidation: true, // Optional, disable client-side validation init: { headers: { 'X-Custom-Header': 'value' } }, // Optional, fetch init apiRoot: 'https://api.example.com', // Optional, override API root URL });

Client-side validation availability of schema

The RPC modules generated by Vovk.ts include built-in, yet optional client-side validation using Ajv  library. This allows to validate input data before sending a request to the server, ensuring that the data conforms to the expected schema. The validation can be disabled by passing disableClientValidation: true option to the method.

import { UserRPC } from 'vovk-client'; await UserRPC.updateUser({ // will throw a validation error if input data is invalid // ... });

Besides runtime validation, the generated code also includes the Vovk.ts schema that can be used for random purposes. Composed client as well as each chunk of segmented client exports schema object that contains the organised and easy to access Vovk.ts schema.

import { schema } from 'vovk-client'; // import { schema } from 'vovk-client/schema';

The schema also accessible at every generated method.

import { UserRPC } from 'vovk-client'; UserRPC.updateUser.schema.validation.body; // JSON Schema for request body

Using function calling

Every RPC module generated by Vovk.ts can be mapped into AI tools, making them accessible by function calling APIs.

import { PetstoreRPC } from 'vovk-client'; const { tools } = createLLMTools({ modules: { PetstoreRPC, } }); console.log(tools); // [{ execute: (llmInput) => {}, name: 'PetstoreRPC_updatePet', description: 'Update an existing pet by Id', parameters: { body: { ... } } }, ...]

Python and Rust clients (experimental)

Vovk.ts templates also support generating Python and Rust clients, that support client-side validation and the fixed set of options. Take a look at Python and Rust pages for more details.

Component-agnostic type inference

A good practice for OpenAPI/Codegen design is to use components/schemas to define input and output data. This allows to generate properly named types, that are accepted by the generated client functions. The problem is that not every OpenAPI specification is designed this way, and turning every input/output bit into a components/schema requires additional effort that’s not always justified.

Due to the lack of component/schemas, codegens will generate types with gibberish names such as ApiUsersIdPostRequest or ApiUsersIdPost200Response, making them hard to use but also making developers to avoid using codegens at all, prefering to use fetch or axios directly, providing URL and casting a type manually.

Vovk.ts is designed to support component-agnostic type inference, meaning that even if the OpenAPI specification doesn’t define components/schemas, Vovk.ts will still be able to infer input and output types with simple utilities.

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

At the previously mentioned Python client, the types are stored as typedicts.

from vovk_client import PetstoreRPC body: PetstoreRPC.UpdateUserBody = {} query: PetstoreRPC.UpdateUserQuery = {} params: PetstoreRPC.UpdateUserParams = {} output: PetstoreRPC.UpdateUserOutput = {}

For the Rust client, the types are generated as nested modules that contain structs.

use vovk_client::petstore_rpc; use user_rpc::update_user_::{ body as Body, body_::profile as Profile, // for nested data query as Query, params as Params, output as Output, };

Bundle

The TypeScript artifacts can be bundled into an NPM package using bundle command that also creates package.json and README.md files, where the README file outlines each method with self-documented code samples. See “Hello World” example for more details.

Note that, in order to create a bundle, you need to have package.json and tsconfig.json files in the root of your project.

Getting started

Install dependencies

In case if the codegen feature is used as a standalone CLI tool (even without package.json), install vovk-cli package globally or as a dev dependency.

npm install -g vovk-cli@draft

or install vovk-cli as a dev dependency and vovk and vovk-ajv as regular dependencies.

npm install -D vovk-cli@draft
npm install vovk@draft vovk-ajv@draft

In case if you’re in another Node.js project, and you want to use composed client (where all generated API clients are combined into a single client), you can also install vovk-client package that re-exports files generated by Vovk.ts at default path node_modules/.vovk-client.

npm install vovk-client@draft

Create config file

You can create a config file as described at config page. Another way to create a config file is to use vovk-cli init command.

npx vovk-cli@draft init --channel draft

A basic config file will look like this:

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

Define OpenAPI mixins

A mixin can be defined as a pseudo-segment in the segments object of the generatorConfig, by setting openAPIMixin property. It expects an object with the following properties:

  • source - an object with either url (for remote specs), path (for local specs) or object (for inline specs). The url variant can also be extended with fallback property, that points to a local file path to be used when the remote URL is not reachable.
  • getModuleName - a string or a function that defines names of the generated RPC modules. The string can be nestjs-operation-id, described at NestJS page, or any other string that will be used as is.
  • getMethodName - a string or a function that defines how the method names are going to be generated. The string can be nestjs-operation-id, described at NestJS page, camel-case-operation-id that converts operationId to camel case (get_users will turn into getUsers), or auto that generates method names automatically from operationId or based on HTTP method and path if operationId isn’t suitable or isn’t set.
  • apiRoot (optional) - a string that defines the root URL of the API, that can be overridden by passing apiRoot option to each method. Required when servers property is not defined in the OAS document.

A Petstore example with remote URL and fallback to a local file:

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

This will generate a single PetstoreRPC module with methods for each operation defined in the OpenAPI specification.

import { PetstoreRPC } from 'vovk-client'; await PetstoreRPC.getPets({ query: { limit: 10 } });

When getModuleName or getMethodName are functions, they receive an object with the following properties:

  • operationObject - the OpenAPI Operation Object for a particular operation.
  • method - the HTTP method (in uppercase) as a string.
  • path - the path of the operation as a string.
  • openAPIObject - the entire OpenAPI document object.

For a more advanced example, let’s create an alternative client for GitHub REST API . The operationId presented at the Github OpenAPI spec  have a shape like scope/operation (for example, repos/remove-status-check-contexts or codespaces/list-for-authenticated-user). We can use the first part of the operationId to generate module names, and the second part to generate method names using lodash.

An operationId that looks like issues/list-for-org will turn into GithubIssuesRPC module with 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 = { generatorConfig: { 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, '')}RPC`; }, getMethodName: ({ operationObject }) => { const [, operationName] = operationObject.operationId?.split('/') ?? ['', 'ERROR']; return camelCase(operationName); }, }, }, }, }, }; export default config;

You may also want to set ajv options to be less strict, as the OAS documents provided by third-party APIs may contain non-standard keywords that will cause validation errors.

vovk.config.js
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { // ... libs: { /** @type {import('vovk-ajv').VovkAjvConfig} */ ajv: { options: { strict: false, // Petstore OAS provides "xml" keyword that causes errors in strict mode }, }, }, }; export default config;

Customize fetcher

You can customize fetching function for each mixin individually, or use a single fetcher for all mixins. The fetcher is a function that is used to prepare handlers, perform client-side validation, make and handle HTTP requests.

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

Composed client

mjs + cjs (default)

By default composed client feature uses mjs and cjs templates to generate both ESM and CJS clients, emitted to node_modules/.vovk-client folder that can be imported as vovk-client package.

import { PetstoreRPC, GithubIssuesRPC, type Mixins } from 'vovk-client'; await PetstoreRPC.getPets({ query: { limit: 10 } }); await GithubIssuesRPC.listForOrg({ params: { org: 'finom' } });

The Mixins namespace contains types generated from components/schemas of all mixed OpenAPI specifications, giving an alternative way to access types.

import { PetstoreRPC, type Mixins } from 'vovk-client'; const pet: Mixins.Pet = { id: 1, name: 'Doggo' }; // ^ alternative to component-agnostic inference const pet: VovkOutput<typeof PetstoreRPC.getPet> = { id: 1, name: 'Doggo' };

ts

ts template generates an uncompiled TypeScript client that can be emitted directly to your codebase.

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { composedClient: { fromTemplates: ['ts'], // use 'ts' instead of 'mjs' and 'cjs' outDir: './src/lib/client', // emit to your codebase prettifyClient: true, // prettify the output }, }; export default config;
import { PetstoreRPC, GithubIssuesRPC, type Mixins } from '../lib/client'; // ...

Segmented client

Segmented client splits the code into multiple chunks, storing each mixin into a separate folder, described as a segment name (petstore, github etc from generatorConfig.segments).

By default, they are emitted to src/client folder, but you can change the output folder by setting outDir property at segmentedClient config.

vovk.config.js
/** @type {import('vovk').VovkConfig} */ const config = { segmentedClient: { outDir: './src/lib/client', // emit to your codebase prettifyClient: true, // prettify the output }, }; export default config;
import { PetstoreRPC, type Mixins as PetstoreMixins } from '../lib/client/petstore'; import { GithubIssuesRPC, type Mixins as GithubMixins } from '../lib/client/github'; // ...

A bonus for this approach is that it also generates an alternative OAS document that contains Scalar -compatible code samples.

import { PetstoreRPC, openapi as petstoreOpenAPI, type Mixins as PetstoreMixins } from '../lib/client/petstore'; import { GithubIssuesRPC, openapi as githubOpenAPI, type Mixins as GithubMixins } from '../lib/client/github'; console.log(petstoreOpenAPI, githubOpenAPI);
Last updated on