Decorators
Controller decorators extend the functionality of controller methods. Use them to implement crossβcutting concerns such as logging, caching, validation, and authorization. They can also attach custom metadata to the handler for purposes like identifying authorized users.
createDecorator is a higherβorder function that produces a decorator factory (a function that returns a decorator) for controller class methods. It accepts a middleware function with the following parameters:
request, which extendsVovkRequest. It provides req.vovk.meta to get and set metadata for sharing data between decorators and the route handler.next, a function you call (and return) to invoke subsequent decorators or the route handler.- Additional arguments passed through the decorator factory.
The second argument to createDecorator is an optional init handler. It runs each time the decorator is initialized and can populate .vovk-schema/*.json with validation or custom data. It may return an object with optional keys "validation", "operationObject", and "misc" to merge into the handler schema, or a function returning that object and receiving the existing handler schema for proper merging.
import { createDecorator, get, HttpException, HttpStatus } from 'vovk';
interface ReqMeta {
foo: string;
a: string;
b: number;
}
const myDecorator = createDecorator(
(req, next, a: string, b: number) => {
console.log(a, b); // Outputs: "foo", 1
req.vovk.meta<ReqMeta>({ foo: 'bar', a, b }); // Add metadata to the request object
if (isSomething) {
// override route method behavior and return { hello: 'world' } from the endpoint
return { hello: 'world' };
}
if (isSomethingElse) {
// throw HTTP error if needed
throw new HttpException(HttpStatus.BAD_REQUEST, 'Something went wrong');
}
// Continue to the next decorator or the route handler
return next();
},
(a: string, b: number) => {
console.info('Decorator is initialized with', a, b);
return {
validation: {
/* ... */
},
misc: { a, b }, // adds `a` and `b` to the handler schema
};
}
);
export default class MyController {
@get.auto()
@myDecorator('baz', 1) // Passes 'baz' as 'a' and 1 as 'b'
static doSomething(req) {
const meta = req.vovk.meta<ReqMeta>();
console.log(meta); // { foo: 'bar', a: 'baz', b: 1 }
// ...
}
}Examples
console.log Decorator
A simple logging decorator that logs the request method and URL before proceeding to the next decorator or route handler.
import { createDecorator } from 'vovk';
const log = createDecorator((req, next, message?: string) => {
console.log(`${message ?? 'Incoming request'}: ${req.method} ${req.url}`);
return next();
});
export default log;Import the log decorator and apply it to controller methods after the @get, @post, etc., decorators.
import { get, prefix } from 'vovk';
import log from '../decorators/log';
@prefix('users')
export default class UserController {
@get('info')
@log('Fetching user info')
static async getUserInfo() {
// ...
}
}Basic Authorization Decorator
Basic authentication is a simple HTTP protocol for user authentication where credentials (username and password) are sent in the Authorization header of a request after being encoded in Base64. While not the most secure method, it can be useful for legacy cross-service communication.
import { HttpException, HttpStatus, createDecorator } from 'vovk';
const basicAuthGuard = createDecorator((req, next) => {
const authorisation = req.headers.get('authorization');
if (!authorisation) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'No authorisation header');
}
const token = authorisation.split(' ')[1];
if (!token) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'No token provided');
}
let login, password;
try {
[login, password] = Buffer.from(token, 'base64').toString().split(':');
} catch (error) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'Unable to parse token. ' + String(error));
}
if (login !== process.env.BASIC_AUTH_LOGIN || password !== process.env.BASIC_AUTH_PASSWORD) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'Invalid login or password');
}
return next();
});
export default basicAuthGuard;Import the basicAuthGuard decorator and apply it to controller methods after the @get, @post, etc., decorators.
import { get, prefix } from 'vovk';
import basicAuthGuard from '../decorators/basicAuthGuard';
@prefix('secure')
export default class SecureController {
@get('data')
@basicAuthGuard()
static async getSecureData() {
// ...
}
}RBAC Decorator
Role-based access control (RBAC) is a method of restricting system access for users based on their role within an organization, rather than assigning permissions individually.
The authGuard decorator below:
- Verifies the user is authorized; otherwise returns an
Unauthorizedstatus. - Adds
currentUserto request metadata, represented by theAuthMetaTypeScript interface. - Implements role-based access control with the
Permissionenum.
The identifyUserAndCheckPermissions function is a placeholder for your logic to identify the user from the request (e.g., from a JWT token or session) and check whether they have the required permission.
import { createDecorator, HttpException, HttpStatus, type VovkRequest } from 'vovk';
import type { User } from '@/types';
export enum Permission {
CAN_DO_THIS = 'CAN_DO_THIS',
CAN_DO_THAT = 'CAN_DO_THAT',
}
// Metadata interface allows access to currentUser in the controller
export interface AuthMeta {
currentUser: User;
}
// Identify the user, check permissions, and update request metadata
const checkAuth = async (req: VovkRequest, permission: Permission) => {
const currentUser = identifyUserAndCheckPermissions(req, permission);
if (!currentUser) {
return false;
}
// Add currentUser to the request metadata
req.vovk.meta<AuthMeta>({ currentUser });
return true;
};
// Create the decorator
const authGuard = createDecorator(async (req, next, permission: Permission) => {
const isAuthorized = await checkAuth(req, permission);
if (!isAuthorized) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'Unauthorized');
}
// The user is authorized and metadata is set; proceed to the next decorator or controller handler
return next();
});
export default authGuard;Import the authGuard decorator and related members, then apply it to controller methods after the @get, @post, etc., decorators.
import { get, prefix } from 'vovk';
import authGuard, { Permission, type AuthMeta } from '../decorators/authGuard';
@prefix('users')
export default class UserController {
// ...
@get('something')
@authGuard(Permission.CAN_DO_THIS)
static async getSomething(req: VovkRequest) {
const { currentUser } = req.vovk.meta<AuthMeta>();
// ...
}
// ...
}Vercel Cron Jobs Authorization Decorator
Vercel Cron JobsΒ require simple authorization via an environment variable. You can implement this by creating a decorator that checks the Authorization header against a secret.
import { HttpException, HttpStatus, createDecorator } from 'vovk';
const cronGuard = createDecorator(async (req, next) => {
if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
throw new HttpException(HttpStatus.UNAUTHORIZED, 'Unauthorized');
}
return next();
});
export default cronGuard;Apply the cronGuard decorator to the controller method that should be protected by the cron job authorization.
import { get, prefix } from 'vovk';
import cronGuard from '../decorators/cronGuard';
@prefix('cron')
export default class CronController {
@get('do-something')
@cronGuard()
static async doSomething() {
// ...
}
}Add a cron job to vercel.json. The schedule field uses standard cron syntax (this example runs daily at midnight).
{
"crons": [
{
"path": "/api/cron/do-something",
"schedule": "0 0 * * *"
}
]
}