Skip to Content
About

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”.

Segment

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 .

src/modules/user/UserController.ts
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); } }
Important

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 (admin segment /api/admin)
      • route.ts (customer segment /api/customer)
        • route.ts (static segment /api/customer/static for OpenAPI)

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 ./schema.ts)
      • schema.ts (imports .vovk-schema/admin.json)
      • 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)

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 for req.json() etc.
    • Client-side, to build typed RPC modules with expected inputs.
  • 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

Last updated on