About
A conceptual back-end meta-framework for Next.js App Router. Designed and developed by Andrey Gubanov . Built for efficiency and unprecedented DX.
Features:
- RESTful APIs + RPC.
- Client-side validation and TypeScript inference.
- First-class OpenAPI support.
- Single point deployment for frontend and backend, as it’s a normal Next.js app.
- Function calling framework that turns your back-end into an AI agent.
- Built on standards and conventions.
Node.js/Next.js back-end framework
The project was heavily inspired by NestJS that’s built on top of Express , a lower-level HTTP framework, extending it with modular architecture for large back-end apps. Vovk.ts achieves similar goal using Next.js Route Handlers as a lower-level HTTP framework, but installed on top of a new or existing Next.js project as a set of secondary dependencies.
One Node.js project is going to contain both front-end and back-end code, saying goodbye to monorepos and corresponding tooling.
More info
”Segment”
Vovk.ts introduces another hierarchical level to back-end called segments that allows to split the back-end into smaller parts 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 multiple 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.
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 be used as a regular back-end framework to build endpoints that follow the 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 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:
// 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 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 are usually oppose each other, but in Vovk.ts they are combined, making RESTful API client thinner and making internal implementation of RPC as clear as possible.
This idea 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 VovkRequest<TBody, TQuery, TParams>
type) and params: TParams
. The class (often called “static class” at this documentation) is initailized (but not instantiated with new
) at route.ts
file, declaring a new RPC module, described below.
A controller can be decomposed into the controller itself and one or more services that contain busines logic (DB/API calls). Service methods can safely infer controller method types, even if the controller method returns the results of the service method, without triggering “explicit any” error on self-references, thanks to a fix #58616 to TypeScript, made by 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 should use an additional check of return type, or manual type annotation, as handle
function isn’t related to its output type, allowing to return any
. Use x satisfies VovkOutput<typeof f>
or annotate the return type manually. This is not required for proper type inference for RPC modules, but might be useful when handler requires strictest type checking.
More info
”RPC Module”
A controller module that is initialized at route.ts
with initSegment
, will be rendered as an RPC module that has the same list of methods with mapped argument signatures. The controller method accepts a request object, while 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 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 also 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.
More info
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
The JSON files follow the same recursive structure as the segments, where root.json
is being a special name for the root segment with an empty string name ""
. _meta.json
file contains additional metadata, such as explicitly emitted fields of 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, that can be imported from vovk-client
. This approach is called “composed RPC client” and it’s useful for single-page applications where you want to have a single import entry 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.
More info
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, generate OpenAPI documentation, etc. 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 automatically, allowing 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 the JSON schema emission and access to the back-end data that was never available to a developer before, a controller method that uses validation is being a source of truth for many things:
- Type inference:
- Server-side, to enhance built-in functionality fo
NextRequest
object and provide types forreq.json()
etc. - Client-side, to build typed RPC modules with expected inputs.
- Server-side, to enhance built-in functionality fo
- Validation:
- Server-side, to validate incoming requests with Zod, Arktype and other libraries and return an error if the data is invalid.
- Client-side, to validate data with Ajv before sending the request and throw an error if the data is invalid in the client.
- Code generation for TypeScript, Rust, Python, from the back-end schema.
- OpenAPI object, exported as
openapi
variable. - Copy-pastable code snippets for READMEs and OpenAPI documentation.
- LLM Function Calling:
- Server-side to execute the function at the current evaluation context.
- Client-side to call the function using HTTP protocol.
- And more, as developer now has access to information about the back-end.
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 via 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);
}
More info
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 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 JSON and FormData
bodies with isForm
flag passed as validation library option. This option does two things: it changes the resulting type signature of the input body
to FormData
instance, and also adds x-isForm
custom property to the body schema, to be able to distinguish it from regular JSON requests and validate it on the client side properly.
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,
});
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 but powerful capability that’s well known for multiple years already. Even being as simple as it is it mostly ignored by companies and developers, considering it as a fancy feature. Libraries and frameworks like AI SDK simplify the process of integrating LLMs and function calling into applications, solving the problem on 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 execute
function that can be mapped to any LLM function calling format.
The functions can be called either on the server side (by using a controllers) or on the client side (by using an RPC modules that call the functions over HTTP), including 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 have defined your controllers with validation, and used @operation
decorator, providing summary
and description
, the OpenAPI doc is generated automatically, and can be accessed as openapi
variable.
import { UserRPC, openapi } from 'vovk-client';
console.log(openapi);
The OpenAPI doc feature includes first class support for Scalar , generating copy-pastable code snippets that include documentation to the input and output data represented in comments. Here is an example of a snippet generated automatically for the “Hello World” example that 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 to mix Vovk.ts RPC modules with ones that are 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
to pass 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 the mixins are validated on the client side as well using the JSON schema extracted from the OpenAPI schema.
More info
Standalone codegen tool
As Vovk.ts is powered by code generation, and supports OpenAPI documents, it can be used as a standalone codegen tool to generate client libraries for TypeScript but also for Python, Rust (experimental) preserving the same interface and structure as in TypeScript but also performing client-side validation out of the box.
More info
Build & shipment
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, ready to be published to PyPI.
More info