ReferenceClass Runtime

Decorators

Controller, route, parameter, response, guard, interceptor, and filter decorators.

Class, route, parameter, response, guard, interceptor, and exception-filter decorators from nextrush/class. For dependency-injection decorators (@Service, @Repository, @Config, @inject), see Dependency Injection.


Installation

$ pnpm add nextrush

Or install @nextrush/class directly:

$ pnpm add @nextrush/class

Required tsconfig.json settings:

tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Quick Start

import 'reflect-metadata'; // Not needed if using the nextrush meta-package
import { Controller, Get, Post, Body, Param } from 'nextrush/class';

@Controller('/users')
class UserController {
  @Get()
  list() {
    return [{ id: 1, name: 'Alice' }];
  }

  @Get('/:id')
  findOne(@Param('id') id: string) {
    return { id, name: 'Alice' };
  }

  @Post()
  create(@Body() data: { name: string }) {
    return { id: Date.now(), ...data };
  }
}

Controller Decorator

Define a controller class with a path prefix or a full options object:

// Simple prefix
@Controller('/users')
class UserController {}

// With options
@Controller({
  path: '/api/v1/users',
  version: 'v1',
  tags: ['users'],
})
class VersionedUserController {}

ControllerOptions

PropertyTypeDescription
path?stringBase path prefix for all routes in this controller
version?stringAPI version prefix (e.g., 'v1' → '/v1/users')
middleware?MiddlewareRef[]Middleware applied to all routes in this controller
tags?string[]Tags for documentation grouping

Route Decorators

Map methods to HTTP endpoints:

@Controller('/items')
class ItemController {
  @Get() // GET /items
  list() {}

  @Get('/:id') // GET /items/:id
  findOne() {}

  @Post() // POST /items
  create() {}

  @Put('/:id') // PUT /items/:id
  replace() {}

  @Patch('/:id') // PATCH /items/:id
  update() {}

  @Delete('/:id') // DELETE /items/:id
  remove() {}

  @Head() // HEAD /items
  head() {}

  @Options() // OPTIONS /items
  options() {}

  @All('/hook') // All methods on /items/hook
  webhook() {}
}

Parameter Decorators

Extract data from requests.

@Body

@Post()
create(@Body() data: CreateUserDto) {
  // data = full parsed body
}

@Post()
createWithField(@Body('name') name: string) {
  // name = body.name
}

@Param

@Get('/:id')
findOne(@Param('id') id: string) {
  // id = route param :id
}

@Get('/:category/:id')
findByCategory(@Param() params: { category: string; id: string }) {
  // params = all route params
}

@Query

@Get()
list(@Query('page') page: string, @Query('limit') limit: string) {
  // ?page=1&limit=10
}

@Get()
search(@Query() query: Record<string, string>) {
  // query = all query params
}
@Get()
info(@Header('authorization') auth: string) {
  // auth = Authorization header
}

@Get()
allHeaders(@Header() headers: Record<string, string>) {
  // headers = all headers
}

@Ctx

import type { Context } from '@nextrush/types';

@Get()
handler(@Ctx() ctx: Context) {
  // Full access to request/response
}

@Req / @Res

Raw Node.js request/response objects:

@Get()
raw(@Req() req: IncomingMessage, @Res() res: ServerResponse) {
  // Raw Node.js objects
}

Parameter Transforms

// Convert to number
@Get('/:id')
findOne(@Param('id', { transform: Number }) id: number) {}

// Validate with Zod
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

@Post()
create(@Body({ transform: CreateUserSchema.parse }) data: z.infer<typeof CreateUserSchema>) {}

Custom Parameter Decorators

Create reusable parameter decorators with createCustomParamDecorator():

import { createCustomParamDecorator } from 'nextrush/class';
import type { Context } from '@nextrush/types';

const CurrentUser = createCustomParamDecorator((ctx: Context) => ctx.state.user);

const ApiKey = createCustomParamDecorator((ctx: Context) => ctx.get('x-api-key'), {
  required: true,
});

@Controller('/users')
class UserController {
  @Get('/me')
  getProfile(@CurrentUser user: User) {
    return user;
  }

  @Get('/data')
  getData(@ApiKey apiKey: string) {
    return { apiKey };
  }
}

createCustomParamDecorator Options

PropertyTypeDescription
requiredboolean= falseThrow MissingParameterError if the extracted value is undefined
transform?TransformFnTransform the extracted value (sync or async)

Custom parameter decorators use the 'custom' param source internally. They can only be used on method parameters, not constructor parameters.


Response Decorators

@SetHeader

Set response headers on a route. Multiple headers stack:

import { SetHeader, Controller, Get } from 'nextrush/class';

@Controller('/api')
class ApiController {
  @SetHeader('X-Custom-Header', 'my-value')
  @SetHeader('Cache-Control', 'no-store')
  @Get('/data')
  getData() {
    return { result: 'ok' };
  }
}

Headers are precomputed at build time and applied before the handler sends a response.

@Redirect

Redirect the request to another URL. Default status code is 302 (Found).

import { Redirect, Controller, Get } from 'nextrush/class';

@Controller('/legacy')
class LegacyController {
  @Redirect('/new-dashboard', 301)
  @Get('/dashboard')
  oldDashboard() {
    // Handler return value is ignored when @Redirect is applied
  }

  @Redirect('/default-page')
  @Get('/home')
  home() {
    // Return a string to override the redirect URL
    return '/custom-page';
  }

  @Redirect('/fallback')
  @Get('/dynamic')
  dynamic() {
    // Return { url?, statusCode? } to override both
    return { url: '/new-location', statusCode: 307 };
  }
}

Override behavior:

Return valueEffect
voidUses the URL and status code from the decorator
stringOverrides the redirect URL
{ url?, statusCode? }Overrides URL and/or status code

The redirect is implemented via the Location header, not ctx.redirect().

@HttpCode

Set a fixed HTTP status code for a route, resolved at build time and applied when the handler returns without setting ctx.status itself.

import { HttpCode, Controller, Post } from 'nextrush/class';

@Controller('/users')
class UserController {
  @HttpCode(201)
  @Post()
  create() {
    return { created: true };
  }
}

Guards

Guards control access to routes by returning a boolean. Write them as functions or as DI-injectable classes.

import type { GuardFn, GuardContext } from 'nextrush/class';

// Simple guard
const AuthGuard: GuardFn = async (ctx) => {
  return Boolean(ctx.get('authorization'));
};

// Guard factory
const RoleGuard =
  (roles: string[]): GuardFn =>
  async (ctx) => {
    const user = ctx.state.user as { role: string } | undefined;
    return user ? roles.includes(user.role) : false;
  };

@UseGuard(AuthGuard)
@UseGuard(RoleGuard(['admin']))
@Controller('/admin')
class AdminController {
  @Get()
  dashboard() {
    return { admin: true };
  }
}
import type { CanActivate, GuardContext } from 'nextrush/class';
import { Service } from 'nextrush/class';

@Service()
class AuthGuard implements CanActivate {
  async canActivate(ctx: GuardContext): Promise<boolean> {
    const token = ctx.get('authorization');
    if (!token) return false;

    const user = await verifyToken(token);
    ctx.state.user = user;
    return Boolean(user);
  }
}

// Use class guard - resolved from DI
@UseGuard(AuthGuard)
@Controller('/protected')
class ProtectedController {}

Guard Execution Order

Guards run in order: class guards first, then method guards.

@UseGuard(ClassGuard1) // 1st
@UseGuard(ClassGuard2) // 2nd
@Controller('/example')
class ExampleController {
  @UseGuard(MethodGuard1) // 3rd
  @UseGuard(MethodGuard2) // 4th
  @Get()
  handler() {}
}

403 vs other statuses from a guard

Returning false produces a GuardRejectionError (403). To return a different status, throw a typed error instead — it propagates unchanged. For example, throw new UnauthorizedError() (from @nextrush/errors) yields a 401. Thrown guard errors are never downgraded to a generic 403.

Guard Context

Guards receive a lightweight context — no response methods, only request data:

GuardContext

PropertyTypeDescription
methodstringHTTP request method
pathstringRequest path
paramsRecord<string, string>Route parameters
queryRecord<string, string | string[] | undefined>Query parameters
headersRecord<string, string | string[] | undefined>Request headers
bodyunknownParsed request body
stateRecord<string, unknown>Mutable state bag shared between middleware and guards
get(name)(name: string) => string | undefinedGet a specific header value

Interceptors

@UseInterceptor runs cross-cutting logic around a handler's execution — usable at both the controller and method level, mirroring @UseGuard.

import { UseInterceptor, Controller, Get } from 'nextrush/class';
import type { Interceptor } from 'nextrush/class';

const timing: Interceptor = async (ctx, next) => {
  const start = Date.now();
  const result = await next();
  console.log(`${ctx.path} took ${Date.now() - start}ms`);
  return result;
};

@UseInterceptor(timing)
@Controller('/users')
class UserController {
  @Get()
  list() {
    return [];
  }
}

Interceptors resolved from a class (InterceptorClass) are resolved from the DI container the same way class guards are.


Exception Filters

Exception filters localize error handling to a controller or a single route. They are opt-in and non-breaking: a route with no filter behaves exactly as before — thrown errors propagate to the global error middleware.

A filter is a class implementing ExceptionFilter. It declares which errors it handles with @Catch(...) and sets the response through ctx. Attach filters with @UseFilter.

import { Catch, Controller, Get, UseFilter } from 'nextrush/class';
import { Service } from 'nextrush/class';
import type { ExceptionFilter } from 'nextrush/class';
import type { Context } from '@nextrush/types';

class EntityNotFoundError extends Error {}

@Service()
@Catch(EntityNotFoundError)
class NotFoundFilter implements ExceptionFilter {
  catch(error: unknown, ctx: Context): void {
    ctx.status = 404;
    ctx.json({ error: 'Not found' });
  }
}

@UseFilter(NotFoundFilter)
@Controller('/users')
class UserController {
  @Get('/:id')
  findOne() {
    throw new EntityNotFoundError('no such user'); // handled by NotFoundFilter (404)
  }
}

Filters are resolved from the DI container like class guards, so they can inject services (loggers, metrics, error mappers). Resolution is lazy — a filter is only resolved when an error is actually thrown and it is about to run.

Matching rules

PropertyTypeDescription
catch types@Catch(A, B)Matches when error instanceof A or B (subclasses included)
catch-all@Catch()No-arg (or no @Catch) matches any error
precedencemethod → classMethod-level filters run before class-level; first match wins
unmatchedrethrowNo matching filter re-throws to the global error middleware

Relation to the global error middleware

Filters sit in front of the global error middleware, not in place of it. An error no filter matches is rethrown unchanged, so the global handler runs exactly as it does today.


Lifecycle Hooks

OnInit and OnShutdown are duck-typed interfaces — no decorator required. A class implementing onModuleInit() and/or onModuleShutdown() is called automatically when discovered through registerControllers/registerModule.

import type { OnInit, OnShutdown } from 'nextrush/class';
import { Service } from 'nextrush/class';

@Service()
class ConnectionPool implements OnInit, OnShutdown {
  async onModuleInit() {
    // open connections
  }

  async onModuleShutdown() {
    // close connections
  }
}

Use isOnInit(target) / isOnShutdown(target) to check whether a class implements the respective hook.


Metadata Readers

Access decorator metadata programmatically:

import {
  getControllerMetadata,
  getRouteMetadata,
  getParamMetadata,
  getAllParamMetadata,
  getControllerDefinition,
  getAllGuards,
  getClassGuards,
  getMethodGuards,
  isController,
  getResponseHeaders,
  getRedirectMetadata,
  getHttpCode,
} from 'nextrush/class';

// Check if class is a controller
isController(UserController); // true

// Get controller metadata
const meta = getControllerMetadata(UserController);
// { path: '/users', version: undefined, middleware: undefined, tags: undefined }

// Get all routes for a controller
const routes = getRouteMetadata(UserController);
// [{ method: 'GET', path: '/', ... }, { method: 'GET', path: '/:id', ... }]

// Get parameter metadata for a specific method
const params = getParamMetadata(UserController, 'findOne');
// [{ source: 'param', index: 0, name: 'id' }]

// Get all guards for a route (class + method guards)
const guards = getAllGuards(UserController, 'findOne');

// Get response headers for a method
const headers = getResponseHeaders(UserController.prototype, 'getData');
// [{ name: 'Cache-Control', value: 'no-store' }]

// Get redirect metadata for a method
const redirect = getRedirectMetadata(UserController.prototype, 'oldDashboard');
// { url: '/new-dashboard', statusCode: 301 }

Also available: getAllFilters, getClassFilters, getMethodFilters, getCatchTypes, getAllInterceptors, getClassInterceptors, getMethodInterceptors, isGuardClass, isValidHttpMethod, isValidParamSource, getConstructorParamTypes.


TypeScript Exports

All types and runtime exports are available from the package entry point.

Complete import reference
import type {
  ControllerMetadata,
  ControllerOptions,
  RouteMetadata,
  RouteOptions,
  RouteMethods,
  ParamMetadata,
  ParamOptions,
  ParamSource,
  BodyOptions,
  QueryOptions,
  HeaderOptions,
  GuardFn,
  GuardContext,
  GuardMetadata,
  CanActivate,
  Guard,
  Interceptor,
  InterceptorClass,
  InterceptorMetadata,
  ExceptionFilter,
  ExceptionFilterClass,
  FilterMetadata,
  Constructor,
  TransformFn,
  MiddlewareRef,
  ControllerDefinition,
  CustomParamExtractor,
  ResponseHeaderMetadata,
  RedirectMetadata,
} from 'nextrush/class';

import type { OnInit, OnShutdown } from 'nextrush/class';

import {
  Controller,
  Get,
  Post,
  Put,
  Patch,
  Delete,
  Head,
  Options,
  All,
  Body,
  Param,
  Query,
  Header,
  Ctx,
  Req,
  Res,
  HttpCode,
  Redirect,
  SetHeader,
  UseGuard,
  UseInterceptor,
  Catch,
  UseFilter,
  createCustomParamDecorator,
  isController,
  isOnInit,
  isOnShutdown,
  getControllerMetadata,
  getRouteMetadata,
  getParamMetadata,
  getAllParamMetadata,
  getControllerDefinition,
  getAllGuards,
  getClassGuards,
  getMethodGuards,
  getAllFilters,
  getClassFilters,
  getMethodFilters,
  getCatchTypes,
  getAllInterceptors,
  getClassInterceptors,
  getMethodInterceptors,
  isGuardClass,
  getResponseHeaders,
  getRedirectMetadata,
  getHttpCode,
  isValidHttpMethod,
  isValidParamSource,
  getConstructorParamTypes,
  DECORATOR_METADATA_KEYS,
} from 'nextrush/class';

Next Steps

Was this helpful?

On this page