About Vovk.ts
Vovk.ts is a conceptual back-end meta-framework built on the Next.js App Router, designed and developed by Andrey Gubanov . It is built for efficiency and an exceptional developer experience.
Features:
- RESTful API
vsRPC. - Client-side validation and TypeScript inference.
- First-class OpenAPI support.
- Supports a wide range of validation libraries, including Standard Schema.
- Single-point deployment for front-end and back-end as a standard Next.js app.
- Function calling framework that turns your app into an AI agent or enables JARVIS-grade Realtime user interfaces.
- Fast.
- Built on standards and conventions.
Node.js/Next.js Back-End Framework
This project is heavily inspired by NestJS , which is built on top of Express , a lower-level HTTP framework. NestJS extends Express with a modular architecture for large back-end applications. Vovk.ts pursues a similar goal by using Next.js Route Handlers as the lower-level HTTP framework and can be added to a new or existing Next.js project as secondary dependencies.
A single Node.js project houses both front-end and back-end code, eliminating the need for monorepos and the tooling they require.
More info
”Segment”
Vovk.ts introduces an additional hierarchy level in the back-end called segments, which lets you split the back-end into smaller, independently configured parts. The traditional hierarchy of back-end → controller → handler becomes 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 at build time, resulting in multiple smaller “back-ends.”
A segment can also be turned into a static segment, compiling its endpoints into static JSON files during the build. This is useful for historical data, OpenAPI documentation, or any other data that should be pre-generated.
More info
RESTful JSON API + RPC
Vovk.ts is a RESTful API back-end framework that embraces widely used patterns. It can serve as a standard back-end framework for endpoints that follow market-wide 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 developers:
GET,POST,PUT,DELETE, etc. - Standard status codes returned by the server:
200 OK,404 Not Found, etc.
On the client side, back-end endpoints can be invoked using the fetch API or any other HTTP client:
// 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 typeFor RESTful API requests, Vovk.ts also implements the RPC (Remote Procedure Call) paradigm, allowing you to call back-end functions as if they were local, while making a standard fetch 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 paradigms are often viewed as opposites, but in Vovk.ts they are combined: the REST client stays lightweight while the internal RPC implementation remains explicit.
This is achieved by combining code generation with TypeScript inference, mapping each RPC call to its corresponding HTTP endpoint.
More info
Controller and Service
HTTP handlers are implemented as decorated static class methods that expect two arguments: req: NextRequest (enhanced with typed req.json, req.nextUrl, etc., using the VovkRequest<TBody, TQuery, TParams> type) and params: TParams. The class is initialized (but not instantiated with new) in the route.ts file, declaring a new RPC module.
A controller can be split into the controller itself and one or more services that contain business logic (database/API calls). Service methods can safely infer controller method types, even when the controller method returns the service method’s result, without triggering “implicit any” type errors on self-references. This is enabled by a TypeScript fix (#58616 ) by Anders Hejlsberg .
Controller
import { put, prefix } from 'vovk';
import UserService from './UserService';
@prefix('users')
export default class UserController {
@put('{id}')
static async updateUser(req: VovkRequest<{ name: string }, { notify: string }>, { id }: { id: string }) {
return UserService.updateUser(await req.json(), req.nextUrl.searchParams.get('notify'), id);
}
}This feature may require an additional check of the return type or manual type annotation because the handle function isn’t intrinsically tied to its output type and can return any. Use x satisfies VovkOutput<typeof f> or annotate the return type manually. While not required for proper type inference in RPC modules, this can be useful when the handler needs the strictest possible type checking.
More info
”RPC Module”
A controller module initialized in route.ts with initSegment is rendered as an RPC module with the same list of methods and mapped argument signatures. While the controller method accepts a request object, the RPC method is mapped to accept input and additional parameters such as headers or custom options, implemented by a custom fetcher function.
import { UserRPC } from 'vovk-client';
const result = await UserRPC.updateUser({
params: { id: '12345' },
body: { name: 'John Doe' },
query: { notify: 'push' },
init: { headers: { 'X-Custom-Header': 'value' } },
successToast: 'User updated successfully',
});
result satisfies { success: boolean };Unlike traditional RPC frameworks, RPC modules have three inputs instead of one, allowing you to pass URL parameters, query parameters, and request body separately.
When a controller method uses a validation library, the incoming HTTP request input is validated automatically on the server side. If invoked from an RPC module, the data is also validated on the client side using the JSON Schema generated from the validation library, letting you catch errors before the request is sent.
More info
Schema
Each segment emits its back-end schema into an individual JSON file used to construct client-side RPC modules. If you have different areas of your application (e.g., root, admin, customer), each area gets its own JSON schema file in the configurable .vovk-schema/ directory. 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/staticfor OpenAPI)
- route.ts (static segment
- route.ts (customer segment
the resulting schema files are located in the .vovk-schema/ directory:
- root.json
- admin.json
- customer.json
- static.json
- _meta.json
The JSON files follow the same recursive structure as the segments, where root.json is a special name for the root segment with an empty string name "". The _meta.json file contains additional metadata, such as explicitly emitted fields from the vovk.config file.
More info
”Composed” vs “Segmented” RPC
By default, Vovk.ts emits a single RPC client that contains all RPC modules from all segments, importable from vovk-client. This approach is the composed RPC client and is useful for single-page applications where you want a single import entry for all RPC modules.
However, in larger applications, exposing the schema for all segments in a single RPC client isn’t always desirable. 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 the segmented RPC client and lets you split the client into smaller TypeScript files that can be imported separately, hiding RPC modules and their corresponding schemas from specific pages. For example, pages responsible for “customer” functionality won’t import “admin” RPC modules, keeping admin implementation details hidden from customer pages.
More info
Validation
Vovk.ts supports any validation library that implements Standard Schema , including Zod 4 , Valibot , Arktype , as well as Zod 3 and class-validator . It can also be extended with any other TypeScript validation library. The only requirement is the ability to generate JSON Schema from validation rules to enable client-side validation, build typed clients for other languages, generate OpenAPI documentation, etc. The primary validation library used in this documentation is Zod 4, but you can choose the one that fits your needs.
Methods that require validation are implemented with a with[LIBRARY] function that accepts validation models and a handle function that receives the request and parameters. The types of the request and parameters are inferred from the validation models automatically, allowing you to use them in the handle function without additional type annotations.
import { z } from 'zod';
import { prefix, post, operation, type VovkOutput } from 'vovk';
import { withZod } from 'vovk-zod';
@prefix('users')
export default class UserController {
@operation({
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>;
},
});
}Thanks to JSON Schema emission and access to back-end data that was previously unavailable to developers, a controller method that uses validation becomes a source of truth for many purposes:
- Type inference:
- Server-side: to enhance the built-in functionality of the
NextRequestobject and provide types forreq.json(), etc. - Client-side: to build typed RPC modules with expected inputs.
- Server-side: to enhance the built-in functionality of the
- Validation:
- Server-side: to validate incoming requests with Zod, Arktype, and other libraries, returning an error if the data is invalid.
- Client-side: to validate data with Ajv before sending the request, instantly throwing an error if the data is invalid on the client.
- Code generation for TypeScript, Rust, and Python from the back-end schema.
- OpenAPI object, exported as the
openapivariable. - Copy-pastable code snippets for READMEs and OpenAPI documentation.
- LLM Function Calling:
- Server-side: to execute the function in the current evaluation context.
- Client-side: to call the function using the HTTP protocol.
- And more, as developers now have access to comprehensive back-end information.
JSON Lines
Vovk.ts includes first-class support for the JSON Lines format—a convenient way to implement “one request, many responses.” It’s ideal for LLM completions and enables new patterns such as progressive responses and polling.
JSON Lines is another output type that uses the iteration validation field and produces the application/jsonl content type if the client includes the Accept: application/jsonl header. If the Accept header doesn’t include application/jsonl, the output is returned as text/plain so the endpoint can be viewed directly in a 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 JSON Lines output can be consumed via disposable async iterators , allowing you 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);
}More info
Custom Responses
Vovk.ts is a thin wrapper around Next.js API routes, so handlers can return any instance of the Response class, including NextResponse or NextResponse.json(), allowing you to use all available Next.js features and any library built for Next.js.
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' });
}
}More info
FormData
Vovk.ts distinguishes between JSON and FormData bodies using the isForm flag passed as a validation library option. This option changes the input body type to a FormData instance, adds the x-isForm custom property to the body schema to distinguish it from regular JSON requests, and updates the OpenAPI specification accordingly.
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 accepts a FormData instance as body input:
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,
});More info
AI Utilities
Function calling is a powerful capability that enables Large Language Models (LLMs) to interact with your code and external systems in a structured way. Instead of just generating text responses, LLMs can understand when to call specific functions and provide the necessary parameters to execute real-world actions (Hugging Face ).
Function calling is simple yet powerful and has been known for years. Despite its simplicity, it’s often overlooked by companies and developers, who treat it as merely a fancy feature. Libraries and frameworks like AI SDK make it easy to integrate LLMs and function calling into applications, solving the problem of how to call a function.
Vovk.ts solves the problem of what to call by providing a framework that turns back-end functions into ready-to-use LLM tools with name, description, parameters, and an execute function that can be mapped to any LLM function calling format.
The functions can be executed on the server side (using controllers) or on the client side (using RPC modules that call the functions over HTTP), including the ability to use remote APIs with OpenAPI Mixins.
import { createLLMTools } from 'vovk';
import ProductController from '@/modules/product/ProductController';
import { UserRPC, GithubIssuesAPI } from 'vovk-client';
import { pick } from 'lodash';
const { tools } = createLLMTools({
modules: {
// call ProductController methods at the current context
ProductController,
// call UserRPC methods over HTTP
UserRPC: pick(UserRPC, ['getUser', 'updateUser']), // only include specific methods of UserRPC
// add third-party OpenAPI mixin
GithubIssuesAPI: [
GithubIssuesAPI,
{
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
},
],
},
});
console.log(tools); // [{ name, description, parameters, execute }, ...]More info
First-Class OpenAPI Support
Vovk.ts generates OpenAPI 3.1 documentation from the back-end schema out of the box. Once you’ve defined your controllers with validation and used the @operation decorator to provide summary and description, the OpenAPI documentation is generated automatically and can be accessed via the openapi variable.
import { UserRPC, openapi } from 'vovk-client';
console.log(openapi);The OpenAPI feature includes first-class support for Scalar , generating copy-pastable code snippets with documentation for input and output represented in comments. Here is an example automatically generated for the “Hello World” example, which you can also view here :
import { UserRPC } from 'vovk-client';
const response = await UserRPC.updateUser({
body: {
// User email
email: "john@example.com",
// User profile object
profile: {
// User full name
name: "John Doe",
// User age
age: 25
}
},
query: {
// Notification type
notify: "email"
},
params: {
// User ID
id: "123e4567-e89b-12d3-a456-426614174000"
},
});
console.log(response);
/*
{
// -----
// Response object
// -----
// Success status
success: true
}
*/More info
OpenAPI Mixins
The Vovk.ts schema and OpenAPI 3.x schema differ but are convertible. This lets you mix Vovk.ts RPC modules with ones generated from OpenAPI schema files, implementing the same calling signature with triple input: params, query, body, and other options available for all RPC modules, such as init for custom headers, apiRoot to override the API root, etc.
import { GithubIssuesAPI } from 'vovk-client';
const issues = await GithubIssuesAPI.list({
query: { filter: 'created' },
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
});Inputs for mixins are also validated on the client side using the JSON Schema extracted from the OpenAPI schema.
More info
Standalone Code Generation Tool
Because Vovk.ts is powered by code generation and supports OpenAPI documents, it can be used as a standalone tool to generate client libraries for TypeScript, Python (experimental), and Rust (experimental). It preserves the same interface and structure as in TypeScript while also performing client-side validation out of the box.
More info
Build & Shipment
The Vovk.ts CLI provides commands to build distributable RPC libraries. To create a TypeScript package with .d.ts files for npm, use the bundle command, which bundles the client into a single package using tsdown (or any other bundler).
npx vovk bundle --out distThis command bundles the RPC modules of all (or selected) segments into a single package in the dist directory, complete with a package.json and a README.md containing auto-generated documentation for the shipped RPC modules. The package name, description, and version are taken from the root package.json or configured in vovk.config. You can then publish the package to npm or another registry.
npm publish distA similar approach can be used to build packages for Python or Rust, allowing you to ship back-end functionality to other languages and platforms.
npx vovk generate --from py --out dist_pyThis command generates a Python/mypy package in the dist_py directory with RPC modules and a README, ready to be published to PyPI.
This use case is demonstrated in the “Hello World” example project, which showcases this and other ideas described in this article.
More info