Skip to Content
OpenAPI Codegen

Code generation via OpenAPI mixins

Vovk.ts is able to mix the existing Vovk.ts app with modules generated from one or more OpenAPI specifications. This allows to easily integrate third-party APIs into your application, or to generate client libraries for your own APIs. The feature doesn’t require Next.js to be installed and can be used to generate code as a standalone CLI tool. This page will cover configuration file options related to code generation, but 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. Let’s call it “Remote imaginary Procedure Call”, while there is no better term. Feel free to start a discussion  if you want to suggest better terminology.

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 form generation and validation, debugging, etc. 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';
import { UserRPC } from 'vovk-client'; UserRPC.updateUser.schema.validation.body; // JSON Schema for request body

Using AI Tools

Every RPC module generated by Vovk.ts can be mapped into AI tools, granting the access to the third-party APIs to the Function Calling.

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 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 expected to be accepted by the generated client functions. Problem is that not all OpenAPI specifications are designed this way, and turning every input/output bit into a components/schema requires additional effort that’s not always justified, especially if there is no specified task given to the developer.

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 try 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 part of typedicts.

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

For Rust client, the types are generated as nested modules with 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 artifacts can be bundled into an NPM package using bundle command that also creates package.json and README.md files, where README documents each method with self-documented code samples. See 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 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 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 the name of the generated RPC module. 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 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.

Let’s create an alternative client for GitHub REST API . The operationId presented at the Github OpenAPI spec  have a shape like scope/operation (foe 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 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 provides "xml" keyword that gives errors in strict mode, proably fixes other issues }, }, }, }; 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.

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. In order to see what it looks like, you can check “Hello World” page.

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