Skip to Content

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 extends VovkRequest. 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 } // ... } }

Authorization Examples

Authorization for HTTP requests can be implemented with decorators that verify whether a user is authorized using any applicable method (e.g., JWT tokens or session cookies) and pass useful contextโ€”such as the current user and their rolesโ€”to the controller handler via metadata, accessible through req.vovk.meta<T>().

Authentication, in turn, can be implemented using standard Next.js methods described in the Next.js authentication documentationย .

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 or simple applications.

src/decorators/basicAuthGuard.ts
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.

src/modules/secure/SecureController.ts
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.

Letโ€™s define a simple authorization decorator that ensures a user is authenticated and has the required permission to access a resource. The authGuard decorator below:

  • Verifies the user is authorized; otherwise returns an Unauthorized status.
  • Adds currentUser to request metadata, represented by the AuthMeta TypeScript interface.
  • Implements role-based access control with the Permission enum.

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.

src/decorators/authGuard.ts
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.

src/modules/user/UserController.ts
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.

src/decorators/cronGuard.ts
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.

src/modules/cron/CronController.ts
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).

/vercel.json
{ "crons": [ { "path": "/api/cron/do-something", "schedule": "0 0 * * *" } ] }
Last updated on