Skip to Content
Segment

Segment

Overview

Vovk.ts introduces an additional hierarchy level in backend architecture called segments, where controllers are initialized, implemented using Next.js Optional Catch-All Segments . Segments allow you to split your backend into smaller, focused serverless functions with different configurations (exported Next.js constants, such as runtime or maxDuration), improving maintainability and performance.

Segment

Each segment owns a specific path, such as /api/foo or /api/bar as focused “mini backends,” similar to how frontend code is split into pages in Next.js. Segments are initialized by calling initSegment in the route.ts file located in [[…slug]] folders, that returns Next.js route handlers (GET, POST, etc.) for that segment. Vovk.ts uses vovk as the slug name, but any valid name works.

When NODE_ENV is set to "development" (by using next dev), each segment exposes a _schema_ endpoint that serves the segment schema. The dev CLI calls this endpoint to retrieve the schema and build JSON files in .vovk-schema/. This avoids importing Node.js modules in Next.js code, allows export const runtime = 'edge' in route.ts, but also simplifies schema retrieval tooling.

Optional Catch-All Segments are used insted of Catch-All Segments  in order to be able to easily implement a root endpoint for that segment.

Creating Segments

Initialize a segment by calling initSegment in route.ts and export the returned route handlers (GET, POST, etc.). The function accepts:

  • controllers — an object with controllers used by the segment. Object keys define generated RPC module names (use a random name if emitSchema is false); values are the controllers.
  • segmentName — the segment name. Defaults to an empty string for the root segment.
  • emitSchema — whether to emit the schema for the segment. Defaults to true.
  • exposeValidation — whether to expose validation data. Defaults to true.
  • onError — a callback invoked on errors with:
    • error: HttpError — the error instance.
    • request: NextRequest — the incoming request (headers, URL, etc.).

The segment file (route.ts) should also export type Controllers = typeof controllers to enable type inference in the RPC client.

The Root Segment

npx vovk new segment

See vovk new documentation

For simple single-page apps, a single root segment is sufficient. In this setup, the backend is bundled into one serverless function when deployed.

Example route.ts for a single-segment app:

src/app/api/[[...vovk]]/route.ts
import { initSegment } from 'vovk'; import UserController from '../../modules/user/UserController'; import PostController from '../../modules/post/PostController'; export const maxDuration = 300; // Next.js route handler option const controllers = { UserRPC: UserController, PostRPC: PostController, }; // export the controllers type to be used in the client code export type Controllers = typeof controllers; // export the Next.js route handlers export const { GET, POST, PUT, DELETE } = initSegment({ controllers, });

The schema for the root segment is stored at .vovk-schema/root.json.

ℹ️

The name root is used only for file naming. In configuration and elsewhere, the root segment name is an empty string.

Multiple Segments

Create multiple segments to split your backend into separate serverless functions. Reasons include:

  • Using different Next.js route handler options or initSegment configurations.
  • Reducing bundle size by splitting code.
  • Separating app areas (e.g., root, admin, customer, customer/public).
  • Supporting multiple API versions (e.g., v1, v2).
  • Creating a static segment (see below) for OpenAPI specs, historical data, etc.

Each segment’s nested folder determines both the API path and the segment name. For example, /src/app/api/segment-name/[[…slug]]/ is served at /api/segment-name. Nesting is unlimited.

For non-root segments, provide segmentName to initSegment:

src/app/api/foo/[[...vovk]]/route.ts
// ... export const { GET, POST, PUT, DELETE } = initSegment({ segmentName: 'foo', controllers, });

The schema for foo is stored at .vovk-schema/foo.json.

For deeper nesting, e.g., /src/app/api/foo/bar/baz/[[…slug]]/, set segmentName to "foo/bar/baz". The schema is stored at .vovk-schema/foo/bar/baz.json.

Static Segment

npx vovk new segment openapi --static # creates a new static segment named "openapi" at src/app/api/openapi/[[...vovk]]/route.ts

Next.js can pre-render API endpoints at build time using generateStaticParams . Vovk.ts provides the controllersToStaticParams helper to take advantage of this and emit static API endpoints for minimal latency. Use it to serve OpenAPI definitions, historical datasets (refreshed periodically via CI/CD), or other infrequently changing data. It also works in Static Export mode  with the output: 'export' Next.js option:

next.config.js
/** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', }; module.exports = nextConfig;

All you need to do is implement generateStaticParams and return controllersToStaticParams with your controller list.

src/app/api/[[...vovk]]/route.ts
// ... export type Controllers = typeof controllers; export function generateStaticParams() { return controllersToStaticParams(controllers); } export const { GET } = initSegment({ controllers });

When deploying to static hosting (e.g., GitHub Pages), include a .json extension in endpoint paths to ensure proper HTTP headers are served.

import { get, prefix } from 'vovk'; @prefix('hello') export default class HelloController { @get('greeting.json') static async getHello() { return { greeting: 'Hello world!' }; } }

This produces an endpoint like: https://vovk.dev/api/hello/greeting.json  (hosted on GitHub Pages).

If you use a custom slug (e.g., /src/app/api/[[...custom]]/route.ts) instead of the default vovk, pass it as the second argument:

export function generateStaticParams() { return controllersToStaticParams(controllers, 'custom'); }

Static Endpoint Parameters

The @get decorator accepts an options object. One of the options, staticParams, lets you enumerate static parameter combinations to simulate conditional routing. The example below shows a single handler that renders six variants for two parameters: section (a | b) and page (1 | 2 | 3).

import { z } from 'zod/v4'; import { procedure, prefix, get, operation } from 'vovk'; @prefix('static-params') export default class StaticParamsController { @operation({ summary: 'Static Params', description: 'Get the static params: section and page', }) @get('{section}/page{page}.json', { staticParams: [ { section: 'a', page: '1' }, { section: 'a', page: '2' }, { section: 'a', page: '3' }, { section: 'b', page: '1' }, { section: 'b', page: '2' }, { section: 'b', page: '3' }, ], }) static getStaticParams = procedure({ params: z.object({ section: z.enum(['a', 'b']), page: z.enum(['1', '2', '3']), }), handle: async (_req, { section, page }) => { return { section, page }; }, }); }

View live example on examples.vovk.dev » 

This builds six JSON files:

Segment Priority

With multiple segments, the most specific (deepest) one takes priority. For example:

  • /src/app/api/[[…slug]]/ — the root segment
  • /src/app/api/foo/[[…slug]]/foo segment
  • /src/app/api/foo/bar/[[…slug]]/foo/bar segment

A request to /api/foo/bar is handled by the foo/bar segment. If it doesn’t match, but the foo segment does, foo handles it. Otherwise, the root segment handles it.

ℹ️

You can change the API folder name from api to anything else via the rootEntry config option, including setting it to an empty to serve the API from the app root.

RPC Client

Whether the API is static or dynamic, you can call it with the same RPC client, including client-side validation and type inference.

const resp = await StaticParamsRPC.getStaticParams({ params: { section: 'a', page: '1', }, }); console.log(resp); // { section: 'a', page: '1' }
Last updated on