Skip to Content
Controller & Procedure

Controller & Procedure

A controller in Vovk.ts is a class that groups endpoint logic as static members. The class is initialized (not instantiated) inside a segment route via initSegment — that’s how its members become reachable HTTP endpoints.

A procedure is one such member: a single endpoint definition, typically decorated with an HTTP method like @get(), @post(), @put(), @patch(), or @del(). Vovk.ts supports two authoring styles, both of which compile to real Next.js Route Handlers:

  • Bare static method(req: NextRequest, params) signature, same as a plain Route Handler. Class-organized, with a codegen-typed RPC client and segments.
  • procedure() wrapper — turns the static method into a typed, validated callable with body / query / params / output schemas. Unlocks .fn() local calls (SSR, server actions, AI tool execution), OpenAPI generation, and AI tool exposure.

Pick the bare style for simple cases; reach for procedure() whenever you want validation or a transport-agnostic callable.

src/modules/user/UserController.ts
import { put, prefix } from 'vovk'; @prefix('users') // optional prefix for all routes in this controller export default class UserController { @put('{id}') static async updateUser(req: NextRequest, { id }: { id: string }) { const data = await req.json(); // ... } }

The class itself is initialized in a segment route by adding it to the controllers object accepted by initSegment:

src/app/api/[[...vovk]]/route.ts
import { initSegment } from 'vovk'; import UserController from '../../../modules/user/UserController'; const controllers = { UserRPC: UserController, }; export type Controllers = typeof controllers; export const { GET, POST, PUT, DELETE } = initSegment({ controllers });

The key of this object defines the name of the resulting RPC module variable used by the client side:

import { UserRPC } from 'vovk-client'; // performs PUT /api/users/69?notify=push const updatedUser = await UserRPC.updateUser({ params: { id: '69' }, query: { notify: 'push' }, body: userData, });

For more information, see TypeScript Client.

Tip

In order to create a root endpoint for a segment, use no prefix and an empty path (or an empty string) in the HTTP decorator.

src/modules/user/UserController.ts
import { get } from 'vovk'; export default class UserController { @get() static async listUsers(req: NextRequest) { // ... } }

Auto-Generated Endpoints

All HTTP decorators provide an .auto method that generates the endpoint name from the method name, making the handler definition more RPC‑like.

src/modules/user/UserController.ts
import { prefix, put } from 'vovk'; @prefix('users') export default class UserController { // creates PUT /api/users/do-something @put.auto() static async doSomething(/* ... */) { // ... } }

Request Headers

A procedure can access any Next.js APIs, such as cookies, headers, and so on via next package imports. See Next.js documentation  for details.

src/modules/user/UserController.ts
import { put, prefix } from 'vovk'; import { cookies, headers } from 'next/headers'; @prefix('users') export default class UserController { @put('{id}') static async updateUser(req: NextRequest, { id }: { id: string }) { const cookieStore = await cookies(); const sessionToken = cookieStore.get('sessionToken'); const headersList = await headers(); const userAgent = headersList.get('user-agent'); // ... } }

Alternatively, use req.headers from the Web Request API : req.headers.get('user-agent').

VovkRequest Type

VovkRequest mirrors the NextRequest type by adding generics for request body (json method) and query (searchParams property) parameters. This allows you to define the expected types for these parts of the request, enabling type-safe access within procedure.

src/modules/user/UserController.ts
import { put, prefix, type VovkRequest } from 'vovk'; import type { User } from '../../types'; @prefix('users') export default class UserController { // Example request: PUT /api/users/69?role=moderator @put('{id}') static async updateUser( req: VovkRequest<Partial<User>, { notify: 'email' | 'push' | 'none' }>, { id }: { id: string } ) { const data = await req.json(); // Partial<User> const notify = req.nextUrl.searchParams.get('notify'); // 'email' | 'push' | 'none' // ... return updatedUser; } }

VovkRequest extends Request but doesn’t extend NextRequest in order to keep the vovk package independent of next package. However, it replicates the documented NextRequest properties such as cookies (with get, getAll, set, delete, has, clear methods) and nextUrl (with basePath, buildId, pathname, search, and typed searchParams).

procedure Function

The procedure function turns a static method into a typed, validated callable. It accepts validation schemas for body, query, params, and output — using any library that implements both Standard Schema  and Standard JSON Schema , such as Zod , Valibot , or Arktype .

It returns an object with a .handle() method that accepts the actual async handler. The handler receives a type-enhanced request as VovkRequest<TBody, TQuery, TParams> and the validated params: TParams value as the second argument.

src/modules/user/UserController.ts
import { procedure, prefix, put } from 'vovk'; import { z } from 'zod'; @prefix('users') export default class UserController { @put('{id}') static updateUser = procedure({ params: z.object({ id: z.uuid() }), body: z.object({ email: z.email() }), query: z.object({ notify: z.enum(['email', 'push', 'none']) }), output: z.object({ success: z.boolean() }), }).handle(async (req, { id }) => { const { email } = await req.json(); const notify = req.nextUrl.searchParams.get('notify'); // ... }); }

If .handle() is not provided, the procedure throws Not Implemented (501) at runtime.

For separating business logic into its own layer, see Services.

HTTP decorator is optional

A procedure created with procedure() does not need an HTTP decorator to work. Without one, it remains a typed validated callable usable via .fn() — for SSR, server components, server actions, AI tool execution, and so on. The decorator is what additionally mounts the procedure as an HTTP endpoint and makes it appear in the generated RPC client. See Calling Procedures Locally for the full reference and patterns like binding standalone procedures into a controller later.

Alternative: decorate Syntax

If you prefer not to use decorators, you can define procedures using the decorate function. decorate returns an object with a .handle() method for the handler. The controller prefix is defined as a static prefix property. This produces the same result as decorators in terms of functionality, types, and generated RPC modules.

src/modules/user/UserController.ts
import { decorate, procedure, put, operation } from 'vovk'; import { z } from 'zod'; class UserController { static prefix = 'users'; static updateUser = decorate( put('{id}'), operation({ summary: 'Update user' }), procedure({ params: z.object({ id: z.uuid() }), body: z.object({ email: z.email() }), query: z.object({ notify: z.enum(['email', 'push', 'none']) }), output: z.object({ success: z.boolean() }), }), ).handle(async (req, { id }) => { const { email } = await req.vovk.body(); const { notify } = req.vovk.query(); // ... }); } export default UserController;

The decorate function applies decorators in the same order as the stacked decorator syntax — the last decorator listed (closest to the handler) is applied first. For handlers without validation, pass a plain async function to .handle():

static listUsers = decorate( get(), ).handle( async (req: VovkRequest) => { // ... } );

See the Decorators Overview page for more details on when to use decorators vs decorate.

procedure Options

body, query, and params

Use body, query, and params to provide input validation schemas. These validate incoming request data before it reaches the controller handler.

output and iteration

Use output and iteration to provide output validation schemas. output is for regular JSON responses, while iteration is for JSON Lines. Both are optional and don’t affect generated RPC typings, but they enable key features like OpenAPI, AI tools, and for Python, Rust, and future clients. These schemas are not used for client-side validation.

contentType

contentType specifies the expected Content-Type for the request body. It can be a string or an array of strings. It affects several areas of the procedure:

  • Client-side body typing — The RPC method’s body type is inferred based on the content type (e.g. FormData for form data, string for text types, File | ArrayBuffer | Uint8Array | Blob for binary types).
  • Server-side body parsingreq.vovk.body() automatically parses the request body into the appropriate shape (parsed JSON object, parsed FormData, string, or File) based on the incoming Content-Type header.
  • 415 enforcement — When contentType is set, requests that arrive without a matching Content-Type header receive a 415 Unsupported Media Type error.
  • Wildcard matching — Supports wildcard patterns such as 'video/*', 'image/*', or '*/*' to accept a range of media types.

See Content Type for a full breakdown of supported types, body parsing behaviour, and examples.

disableServerSideValidation

Disables server-side validation for the specified library. Provide a boolean to disable it entirely, or an array of validation types (body, query, params, output, iteration). This does not affect generated RPC typings or client-side validation.

skipSchemaEmission

Skips emitting JSON Schema for the handler. Provide a boolean to skip entirely, or an array of validation types (body, query, params, output, iteration). This does not change RPC typings but disables features that depend on emitted schemas, including client-side validation.

validateEachIteration

Applies only to iteration. Controls whether to validate each item in the streamed response. By default, only the first iteration item is validated.

operation

Optionally provide an operation object to specify OAS details when the @operation decorator is not applicable—useful with fn on a regular function instead of a class method.

preferTransformed = true

By default, methods provided by req.vovk transform (but not the built-in Next.js functions such as req.json() or req.nextUrl.searchParams.get()) incoming data into the validation result shape. If you need raw I/O without transformations, set preferTransformed to false. This causes all features that rely on validation models (body, query, params, output, iteration) to return the original data format instead of the transformed one.

procedure Features

Procedures created with procedure() gain extra capabilities beyond plain handlers.

fn

The fn property calls the procedure directly without making an HTTP request — for SSR/PPR, server actions, AI tool execution, etc. Same call shape as the generated RPC method:

const result = await UserController.updateUser.fn({ body: { /* ... */ }, query: { /* ... */ }, params: { /* ... */ }, disableClientValidation: false, // default }); // same call shape as the RPC client const result = await UserRPC.updateUser({ body: { /* ... */ }, query: { /* ... */ }, params: { /* ... */ }, disableClientValidation: false, // default });

See Calling Procedures Locally for details.

schema

const schema = UserController.updateUser.schema; // same as UserRPC.updateUser.schema

The schema property exposes the method schema, mirroring the RPC method schema. It’s typically used with fn to build AI tools that invoke handlers without HTTP.

definition

const bodyModel = UserController.updateUser.definition.body;

The definition property is available only on server-side methods, but not on the RPC methods. It lets you access the original procedure definition.

Last updated on