About
A conceptual back-end meta-framework for Next.js App Router, designed and developed by Andrey Gubanov . Built for efficiency and an unprecedented developer experience.
Features:
- RESTful API
vsRPC. - Client-side validation and TypeScript inference.
- First-class OpenAPI support.
- Single-point deployment for front-end and back-end as a standard Next.js app.
- Function calling framework that transforms your back-end into an AI agent.
- Built on standards and conventions.
Node.js/Next.js Back-End Framework
This project was 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 achieves a similar goal by using Next.js Route Handlers as the lower-level HTTP framework, installed on top of a new or existing Next.js project as a set of secondary dependencies.
A single Node.js project contains both front-end and back-end code, eliminating the need for monorepos and their corresponding tooling.
More info
”Segment”
Vovk.ts introduces an additional hierarchical level to the back-end called segments, which allows you to split the back-end into smaller, independently configured parts. Each segment emits its RPC schema separately. The traditional hierarchy of back-end → controller → handler
is 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 during the build process, creating multiple smaller “back-ends”.
A segment can be converted into a static segment, which compiles its endpoints into static JSON files during build time. This is useful for historical data, OpenAPI documentation, or any other data that should be generated at build time.
More info
RESTful JSON API + RPC
Vovk.ts is a RESTful API back-end framework that implements well-known and widely used patterns. It can serve as a standard back-end framework to build 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 type
For RESTful API requests, Vovk.ts also implements the RPC (Remote Procedure Call) paradigm, which allows you to call back-end functions as if they were local, 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 two concepts typically oppose each other, but in Vovk.ts they are combined, making the RESTful API client thinner while keeping the internal RPC implementation as clear as possible.
This is achieved by combining code generation with TypeScript inference, mapping each RPC call onto a specially constructed URL.
More info
Controller and Service Modules
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 (often referred to as a “static class” in this documentation) is initialized (but not instantiated with new
) in the route.ts
file, declaring a new RPC module as described below.
A controller can be decomposed 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 results of the service method, without triggering “explicit any” errors on self-references. This is thanks to a fix (#58616 ) to TypeScript made by the legendary 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 requires an additional check of the return type or manual type annotation, as the handle
function isn’t inherently related to its output type, allowing it to return any
. Use x satisfies VovkOutput<typeof f>
or annotate the return type manually. While this isn’t required for proper type inference in RPC modules, it can be useful when the handler requires the strictest type checking.
More info
”RPC Module”
A controller module that is initialized in route.ts
with initSegment
will be 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 accepts 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, Vovk.ts RPC modules have 3 inputs instead of 1, allowing you 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. If invoked from an RPC module, the data is also validated on the client side using the JSON schema generated from the validation library. This allows you to catch errors early on the client side before the request is sent to the server.
More info
Schema
Each segment emits its back-end schema into an individual JSON file 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 in a configurable directory .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 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, which can be imported from vovk-client
. This approach is called 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. 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 the “segmented RPC client” and allows you to 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 that the library must be able to generate JSON schema from validation rules to validate data on the client side, build typed clients, 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 any 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
NextRequest
object 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, 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
openapi
variable. - 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, which is a convenient way to implement “one request - many responses”. It’s perfect for LLM completions and also opens up new possibilities for experiments, 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-side includes the Accept: application/jsonl
header. If the Accept
header doesn’t include application/jsonl
, the output is returned as text/plain
to be viewable when the endpoint URL is opened 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 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.
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 serves two purposes: it changes the resulting type signature of the input body
to a FormData
instance and adds the x-isForm
custom property to the body schema to distinguish it from regular JSON requests and validate it properly 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 a FormData
instance as the 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,
});
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 a simple yet powerful capability that has been well-known for several years. Despite its simplicity, it’s often overlooked by companies and developers, who consider it merely a fancy feature. Libraries and frameworks like AI SDK simplify the process of integrating 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 to build back-end functions that are easily converted into LLM tools with name
, description
, parameters
, and an execute
function that can be mapped to any LLM function calling format.
The functions can be called either on the server side (using controllers) or on the client side (using RPC modules that call functions over HTTP), including the capability to use remote APIs with OpenAPI mixins.
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 of UserRPC
GithubIssuesRPC: [ // add OpenAPI mixin
GithubIssuesRPC,
{
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 as 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 data represented in comments. Here is an example of a snippet 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
Vovk.ts schema and OpenAPI 3+ schema are different but convertible to each other. This allows you to 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 passing custom headers, apiRoot
to override the API root, etc.
import { 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',
},
},
});
The 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
As Vovk.ts is powered by code generation and supports OpenAPI documents, it can be used as a standalone code generation tool to generate client libraries for TypeScript, Python, 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 packages. For example, to build a package for NPM, you can use the bundle
command, which will bundle the client into a single package using tsdown behind the scenes.
npx vovk bundle --out dist
This command will bundle the RPC modules of all or specified segments into a single package in the dist
directory, complete with a package.json
file and README.md
containing auto-generated documentation for the shipped RPC modules. The package name, description, and version are taken from either the root package.json
or can be configured in vovk.config. All that’s left is to publish the package to NPM or any other package registry.
npm publish dist
A 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_py
This command will generate a Python/mypy package in the dist_py
directory with all RPC modules, ready to be published to PyPI.
More info