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.
| Decorator | HTTP 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>[]— (@getonly) static params forgenerateStaticParams(). 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 (
experimentalDecoratorsor 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 }));
}