Skip to Content
🐺 Vovk.ts is released. Read the blog post →
Decorators Overview

Decorators Overview

Vovk.ts uses decorators to attach metadata and behavior to controller methods. This page gives a comprehensive overview of all built-in decorators, the decorate alternative, and guidance on when to use each approach.

HTTP Method Decorators

HTTP method decorators define the HTTP method and path for a procedure. They are the only required decorator for a procedure to be reachable via HTTP.

DecoratorHTTP Method
@get()GET
@post()POST
@put()PUT
@patch()PATCH
@del()DELETE
@head()HEAD
@options()OPTIONS

Each accepts an optional path and options object:

import { get, prefix } from 'vovk'; @prefix('users') export default class UserController { @get('{id}', { cors: true, headers: { 'x-custom': 'value' } }) static getUser(req, { id }: { id: string }) { return { id }; } }

Options:

  • cors?: boolean — adds CORS headers and handles OPTIONS automatically.
  • headers?: Record<string, string> — custom response headers. See Response Headers for dynamic headers.
  • staticParams?: Record<string, string>[] — (@get only) static params for generateStaticParams(). See Static Segment for details.

Auto-Generated Paths

Every HTTP decorator provides an .auto() method that derives the path from the method name in kebab-case:

export default class UserController { // creates GET /api/get-all-users @get.auto() static getAllUsers() { return []; } }

Class Decorators

@prefix

Prepends a sub-path to all endpoints in a controller:

import { prefix, get } from 'vovk'; @prefix('users') export default class UserController { @get('{id}') // => GET /api/users/{id} static getUser() { /* ... */ } }

@operation

Attaches OpenAPI metadata to a procedure:

import { operation, get } from 'vovk'; export default class UserController { @operation({ summary: 'Get user by ID', description: 'Returns a single user' }) @get('{id}') static getUser() { /* ... */ } }

Also provides @operation.error() for documenting error responses and @operation.tool() for AI tool metadata.

@cloneControllerMetadata

Copies all metadata from a parent controller to a child class, useful for reusing a controller in multiple segments:

import { prefix, cloneControllerMetadata } from 'vovk'; import UserController from './UserController'; @cloneControllerMetadata() @prefix('v2') export default class UserControllerV2 extends UserController {}

Custom Decorators

Use createDecorator to build custom middleware-style decorators for cross-cutting concerns like authentication, logging, or caching. See the Custom Decorators page for full API documentation and the Decorator Examples page for practical patterns.

import { createDecorator, get, HttpException, HttpStatus, type VovkRequest } from 'vovk'; const authGuard = createDecorator(async (req: VovkRequest, next) => { const token = req.headers.get('authorization'); if (!token) throw new HttpException(HttpStatus.UNAUTHORIZED, 'Missing token'); req.vovk.meta({ userId: await verifyToken(token) }); return next(); }); export default class UserController { @get('{id}') @authGuard() static getUser(req: VovkRequest) { const { userId } = req.vovk.meta(); // ... } }

decorate Function

The decorate function provides an alternative to the stacked decorator syntax. Instead of using @decorator annotations, you pass decorator results to decorate (which returns { handle }). The controller prefix is defined via static prefix. This is useful when you want to avoid decorators entirely or need more flexibility in how procedures are defined.

import { decorate, get, post, operation, HttpStatus, procedure } from 'vovk'; import { z } from 'zod'; class UserController { static prefix = 'users'; static updateUser = decorate( post('{id}'), operation({ summary: 'Update user', description: 'Updates a user by ID', }), operation.error(HttpStatus.BAD_REQUEST, 'Invalid input'), procedure({ params: z.object({ id: z.string() }), body: z.object({ email: z.email() }), query: z.object({ notify: z.enum(['email', 'push', 'none']) }), }), ).handle(async (req, { id }) => { const body = await req.vovk.body(); const { notify } = req.vovk.query(); return { id, ...body, notify }; }); } export default UserController;

All arguments to decorate are decorator results; decorate returns an object with a .handle() method that accepts the handler (or a procedure can be included as one of the arguments). The prefix is set as a static prefix property on the class — equivalent to using the @prefix() decorator.

With Custom Decorators

Custom decorators created with createDecorator work with decorate as well:

static getUser = decorate( get('{id}'), authGuard(), ).handle(async (req: VovkRequest) => { const { userId } = req.vovk.meta(); // ... });

Without Validation

For handlers that don’t need validation, pass a plain function to .handle():

static listUsers = decorate( get(), ).handle( async () => { return []; } );

Decorator Syntax vs decorate

Both approaches produce identical results in terms of functionality, types, and generated RPC modules. Choose based on your preference and project conventions.

When to Use Decorators

  • You’re already using TypeScript decorators in your project.
  • You prefer the visual separation of concerns that stacked decorators provide.
  • You want the most concise syntax for simple procedures.
@operation({ summary: 'Get user' }) @get('{id}') @authGuard() static getUser = procedure({ params: z.object({ id: z.string() }), }).handle(async (req, { id }) => { return { id }; });

When to Use decorate

  • You want to avoid decorators (experimentalDecorators or TC39 Stage 3).
  • You prefer a functional composition style.
  • You want all metadata for a procedure in one expression.
class UserController { static prefix = 'users'; static getUser = decorate( get('{id}'), authGuard(), operation({ summary: 'Get user' }), procedure({ params: z.object({ id: z.string() }), }), ).handle(async (req, { id }) => { return { id }; }); } export default UserController;

Mixing Both

You can mix decorator and decorate syntax within the same controller:

@prefix('users') export default class UserController { // Decorator syntax @get() static listUsers = procedure().handle(async () => []); // decorate syntax static getUser = decorate( get('{id}'), procedure({ params: z.object({ id: z.string() }), }), ).handle(async (req, { id }) => ({ id })); }
Last updated on