Core concepts
βSegmentβ
Vovk.ts introduces another hierarchical level to back-end called segments that allows to split the back-end into smaller sections configured differently that emit RPC schema separately for each segment. Traditional hierarchy that looks like back-end -> controller -> handler
gets extended to back-end -> segment -> controller -> handler
. Each segment is a separate Next.js route that can be configured independently and compiled into a separate serverless function on build, creating smaller βback-endsβ.
A segment can be turned into a βstatic segmentβ that makes its endpoints to be compiled into static JSON files during the build time, allowing to use them for historical data, OpenAPI documentation, or any other data that is generated on build time.
RESTful API RPC
Vovk.ts is a RESTful API back-end framework that implements well-known and widely used patterns. It can be used as a regular back-end framework similar to NestJS to build endpoints that follow common RESTful API conventions, such as:
- Naming conventions for URLs and endpoints:
/users
,/products/{id}
- Query parameters for filtering and pagination:
?page=1&limit=10
- HTTP methods explicitly defined by a developer:
GET
,POST
,PUT
,DELETE
etc. - Status codes returned by the server:
200 OK
,404 Not Found
, etc. and so on.
On the client side, the back-end endpoints can be invoked using the fetch
API or any other HTTP client library, such as Axios, with the same conventions as any other RESTful API:
// Update user with ID 12345
const resp = await fetch('/api/users/12345?notify=push', {
method: 'PUT',
body: JSON.stringify({ name: 'John Doe' }),
headers: {
'Content-Type': 'application/json',
},
});
const result = (await resp.json()) as UserType; // some user type
For the RESTful API requests, Vovk.ts also implements RPCβ (Remote Procedure Call) paradigm that allows to call back-end functions as if they were local, making a normal RESTful API request under the hood.
import { UserRPC } from 'vovk-client';
const result = await UserRPC.updateUser({
params: { id: '12345' },
body: { name: 'John Doe' },
query: { notify: 'push' },
});
result satisfies UserType;
These two concepts are usually oppose each other, but in Vovk.ts they are combined, making RESTful API client thinner and moving away from the idea of hidden implementation of RPC calls.
Why is it called βRPCβ?
Traditional RPC frameworks send a request directly to the server, explicitly invoking a method and waiting for its return value. Vovk RPC follows the same core principleβremote invocation of server-side logicβbut layers in an extra transport step: it maps each RPC call onto a specially constructed URL.
How it Works?
- Request Encoding. The client encodes the target method name and its arguments into a URL.
/api/users/12345?notify=push
translates to a call to theupdateUser
method of theUserRPC
module withid
parameter set to12345
andnotify
query parameter set topush
. - URL Dispatching. When the server receives this URL, its routing logic parses the path to identify the correct controller and method handler.
- Invocation. The handler is invoked with the parsed parameters, executes the business logic, and returns the result.
Isnβt it just CodeGen?
Yes, definitely. RPC is a paradigm, while CodeGen is a process. At this case code generation is used as a tool to achieve RPC experience and matching between controllers and resulting RPC modules, infering types directly from the TypeScript files where controllers are defined.
βRPC Moduleβ
Server-side controllers are implemented as classes with static methods only (called βstatic classesβ in this documentation) that are initialized at the segment route.ts
file with initSegment
function. The controllers
option of the initSegment
defines names of the resulting RPC modules, that have similar shape with different arguments signatures.
Controller
import { put, prefix } from 'vovk';
@prefix('users')
export default class UserController {
@put('{id}')
static async updateUser(req: VovkRequest<{ name: string }, { notify: string }>, { id }: { id: string }) {
console.log('Body', await req.json()); // { name: 'John Doe' }
console.log('Query', req.nextUrl.searchParams.get('notify')); // 'push'
console.log('Param', id); // '12345'
return { success: true };
}
}
Turned into an RPC module:
import { UserRPC } from 'vovk-client';
const result = await UserRPC.updateUser({
params: { id: '12345' },
body: { name: 'John Doe' },
query: { notify: 'push' },
});
result satisfies { success: boolean };
Unlike traditional RPC frameworks, Vovk.ts RPC modules have 3 inputs instead of 1, allowing to pass URL parameters, query parameters, and request body separately.
When a controller method is implemented with a validation library, the incoming HTTP request input is validated automatically on the server side, and if itβs invoked from an RPC module, the data is validated on the client side as well using the JSON schema generated from the validation library. This allows to catch errors early on the client side, before the request is sent to the server.
Schema
Each segment emits back-end schema into an individual JSON file that is used to generate client-side RPC modules. If you have different areas of your application (e.g. root, admin, customer), each area will have its own JSON schema file located at a configurable dir .vovk-schema/
. For a segment structure like this:
- route.ts (root segment
/api/
)
- route.ts (root segment
- route.ts (admin segment
/api/admin
)
- route.ts (admin segment
- route.ts (customer segment
/api/customer
) - route.ts (static segment
/api/customer/static
for OpenAPI)
- route.ts (static segment
- route.ts (customer segment
the resulting schema files will be located in .vovk-schema/
directory like this:
- root.json
- admin.json
- customer.json
- static.json
- _meta.json
Each individual schema file represents a segment and contains and contains all RPC modules defined in that segment. It follows the same recursive structure as the segments, where root.json
is a special name for the root segment with name ""
. _meta.json
file contains additional metadata, such as explicitly emitted fields of the vovk.config file.
βComposedβ vs βsegmentedβ RPC client
By default Vovk.ts emits a single RPC client that contains all RPC modules from all segments. This approach is called βcomposed RPC clientβ and itβs useful for single-page applications where you want to have a single entry point for all RPC modules.
However, in larger applications, exposing schema for all segments in a single RPC client isnβt always desirable. In this case, with a simple configuration change, you can instruct Vovk.ts to generate separate RPC clients for each segment.
- index.ts (imports
./schema.ts
) - schema.ts (imports
.vovk-schema/root.json)
- index.ts (imports
- index.ts (imports
./schema.ts
) - schema.ts (imports
.vovk-schema/admin.json)
- index.ts (imports
- index.ts (imports
./schema.ts
) - schema.ts (imports
.vovk-schema/customer.json
) - index.ts (imports
./schema.ts
) - schema.ts (imports
.vovk-schema/customer/static.json
)
- index.ts (imports
- index.ts (imports
This approach is called βsegmented RPC clientβ and it allows to split the client into smaller TypeScript files that can be imported separately, hiding RPC modules and corresponding schema on specific pages. For example, pages responsible for βcustomerβ functionality isnβt going to import βadminβ RPC modules, therefore βadminβ implementation details are hidden from the customer pages.
Validation
Vovk.ts supports any validation library that implements Standard Schemaβ, including Zod 4β, Valibotβ, Arktypeβ, but also Zod 3β, class-validatorβ and can be extended with any other TypeScript validation library. The only requirement is that the library should be able to generate JSON schema from the validation rules in order to validate the data on the client side, build typed client, and generate OpenAPI documentation. The primary validation library that is used in the documentation is Zod 4 but itβs up to you to choose the one that fits your needs.
Methods that need to be validated are implemented with with[LIBRARY]
function that accepts validation models and handle
function that receives the request and the parameters. The types of the request and parameters are inferred from the validation models, allowing to use them in the handle
function without any additional type annotations.
import { z } from 'zod';
import { prefix, post, openapi, type VovkOutput } from 'vovk';
import { withZod } from 'vovk-zod';
@prefix('users')
export default class UserController {
@openapi({
summary: 'Update user (Zod)',
description: 'Update user by ID with Zod validation',
})
@post('{id}')
static updateUser = withZod({
body: z
.object({
name: z.string().describe('User full name'),
age: z.number().min(0).max(120).describe('User age'),
email: z.email().describe('User email'),
})
.describe('User object'),
params: z.object({
id: z.uuid().describe('User ID'),
}),
query: z.object({
notify: z.enum(['email', 'push', 'none']).describe('Notification type'),
}),
output: z
.object({
success: z.boolean().describe('Success status'),
})
.describe('Response object'),
async handle(req, { id }) {
const { name, age } = await req.json();
const notify = req.nextUrl.searchParams.get('notify');
// do something with the data
console.log(`Updating user ${id}:`, { name, age, notify });
return {
success: true,
} satisfies VovkOutput<typeof UserZodController.updateUser>;
},
});
}
JSON Lines
Vovk.ts includes first class support for JSON Linesβ format, which is a convenient way to implement βone request - many responsesβ. Itβs perfect for LLM completions, but also opens up a new field for experiments, such as progressive responses and polling. JSONLines is another kind of output that uses iteration
validation field and produces application/jsonl
content-type if client-side includes Accept: application/jsonl
header. If the Accepts
header doesnβt include application/jsonl
, the output is returned as text/plain
to be available when the endpoint URL is opened directly in the browser.
import { z } from 'zod';
import { prefix, post, type VovkIteration } from 'vovk';
import { withZod } from 'vovk-zod';
@prefix('stream')
export default class StreamController {
@post('completions')
static getJSONLines = withZod({
// ...
iteration: z.object({
message: z.string(),
}),
async handle() {
const tokens: VovkIteration<typeof StreamController.getJSONLines>[] = [
{ message: 'Hello,' },
{ message: ' World' },
{ message: ' from' },
{ message: ' Stream' },
{ message: '!' },
];
for (const token of tokens) {
await new Promise((resolve) => setTimeout(resolve, 300));
yield token;
}
},
});
}
On the client side, the JSONLines output can be consumed using disposable async iterators, allowing to process each line as it arrives:
import { StreamRPC } from 'vovk-client';
using stream = await StreamRPC.getJSONLines();
for await (const { message } of stream) {
console.log('Received message:', message);
}
Custom responses
Vovk.ts is a wrapper around Next.js API routes, so the handlers can return any instance of Response
class, including NextResponse
or NextResponse.json()
, allowing to use all Next.js features, such as cookies, headers, and so on. Vovk.ts is designed to use existing Next.js features, and never reinvent the wheel.
import { NextResponse, headers } from 'next/server';
import { prefix, get } from 'vovk';
@prefix('users')
export default class UsersController {
@get('{id}')
static async getUser() {
console.log('Headers:', await headers());
return NextResponse.json({ name: 'John Doe' });
}
}
FormData
Vovk.ts distinguishes JSON and FormData
bodies with isForm
flag passed as validation option. This option does two things: it changes the resulting type signature of the input body
to FormData
instance, and also adds x-formData
custom option to the resulting handler schema, to be able to distinguish it from regular JSON requests and validate it on the client side.
export default class UserController {
@post('create')
static createUser = withZod({
body: z.object({
name: z.string(),
age: z.number(),
}),
isForm: true, // this will change the body type to FormData
async handle(req) {
const formData = await req.formData();
// ...
},
});
}
On the client-side the createUser
RPC method will accept FormData
instance as a body:
import { UserRPC } from 'vovk-client';
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('age', '30');
const response = await UserRPC.createUser({
body: formData,
});
OpenAPI mixins & Codegen
The client-side RPC modules can be mixed with OpenAPI schema files, creating a pseudo-segment called in this documentation βOpenAPI Mixinβ. The provided OpenAPI schema is converted into Vovk.ts Schema under the hood and the generated client gets mixed with the existing RPC modules, allowing to call OpenAPI endpoints as if they were RPC modules. This is useful for integrating with existing services that provide OpenAPI schema.
// UserRPC is a local RPC module defined in the segment
// GithubIssuesRPC is generated from OpenAPI schema file
import { UserRPC, GithubIssuesRPC } from 'vovk-client';
const issues = await GithubIssuesRPC.list({
query: { filter: 'created' },
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
});
Unlike traditional codegen tools, Vovk.ts splits the resulting library into smaller RPC modules, allowing to import only the necessary parts of the library and split the code into smaller logical chunks, but also provides a unified interface with fixed list of input parameters, such as params
, query
, and body
, making it easier to use and understand.
Vovk.ts also includes code generator templates for Python and Rust, allowing to easily generate client libraries for these languages, preserving similar interface and structure as in TypeScript.
Python example:
npx vovk generate --from py --out dist_py
from my_module import UserRPC
body: UserRPC.UpdateUserBody = {
'name': 'John Doe',
'age': 30,
}
query: UserRPC.UpdateUserQuery = {
'notify': 'push',
}
params: UserRPC.UpdateUserParams = {
'id': '12345',
}
result = UserRPC.update_user(
body,
query,
params,
)
Rust example:
npx vovk generate --from rs --out dist_rs
use my_module::user_rpc;
use user_rpc::update_user_::{ // _:: syntax accesses the types of a nested structure
body as Body,
query as Query,
params as Params
};
let result = user_rpc::update_user(
Body { name: String::from("John Doe"), age: 30 },
Query { notify: String::from("push") },
Params { id: String::from("12345") },
None, // headers
None, // custom API root
false, // disable_client_validation
);
match result {
Ok(response) => println!("user_rpc.update_user: {:?}", response),
Err(e) => println!("update_user error: {:?}", e),
}
AI utilities
Schema availability and code splitting into controllers and RPC modules makes it easy to implement LLM Function Calling and MCP server using existing AI ecosystem. vovk
package exports createLLMTools
that returns an array of tools with name
, description
, parameters
and execute
function that can be mapped to any LLM Function Calling format. It accepts a list of modules that are used to generate the tools, including RPC modules but also controllers, allowing to execute controller methods directly in the current context, without performing an HTTP request.
import { createLLMTools } from 'vovk';
import ProductController from '@/modules/product/ProductController';
import { UserRPC, GithubIssuesRPC } from 'vovk-client';
import { pick } from 'lodash';
const { tools } = createLLMTools({
modules: {
ProductController, // call ProductController methods at the current context
UserRPC: pick(UserRPC, ['getUser', 'updateUser']), // only include specific methods
GithubIssuesRPC: [
GithubIssuesRPC,
{
// add OpenAPI mixin
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
},
],
},
});
console.log(tools); // [{ name, description, parameters, execute }, ...]
Ready to be shipped
Vovk.ts CLI provides commands to build distributable packages. For example, to build a package for NPM, you can use bundle
command that will bundle the client into a single package with tsdownβ behind the scenes.
npx vovk bundle --out dist
The command will bundle the RPC modules of all or specified segments into a single package in the dist
directory, with package.json
file and README.md
that contains auto-generated documentation for the shipped RPC modules. Package name, description and version are taken either from the root package.json
or can be configured at vovk.config. The only thing left is to publish the package to NPM or any other package registry.
npm publish dist
Similar approach can be used to build a package for Python or Rust, allowing to ship the back-end functionality to other languages and platforms.
npx vovk generate --from py --out dist_py
The command will generate a Python/mypy package in the dist_py
directory with all RPC modules and OpenAPI schema files, ready to be published to PyPI.
For more information, take a look at βHello Worldβ example.