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.
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:
/** @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 eitherurl
(for remote specs),path
(for local specs) orobject
(for inline specs). Theurl
variant can also be extended withfallback
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 benestjs-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 benestjs-operation-id
, described at NestJS page,camel-case-operation-id
that convertsoperationId
to camel case (get_users
will turn intogetUsers
), orauto
that generates method names automatically fromoperationId
or based on HTTP method and path ifoperationId
isn’t suitable or isn’t set.apiRoot
(optional) - a string that defines the root URL of the API, that can be overridden by passingapiRoot
option to each method. Required whenservers
property is not defined in the OAS document.
A Petstore example with remote URL and fallback to a local file:
/** @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.
// @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.
// @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.
/** @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.
/** @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.
/** @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);