npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nextrush/decorators

v3.0.6

Published

Metadata-only decorators for NextRush controllers

Readme

@nextrush/decorators

Decorator-based metadata for building HTTP controllers with guards, dependency injection, and type-safe parameter extraction.

The Problem

Building structured APIs in Node.js often leads to scattered route definitions, manual parameter parsing, and inconsistent authentication checks. Without a declarative system:

  • Route handlers mix business logic with request parsing
  • Authentication checks are copy-pasted across handlers
  • Parameter validation is ad-hoc and error-prone
  • Testing requires mocking the entire HTTP layer

How NextRush Approaches This

NextRush decorators provide declarative contracts for HTTP controllers:

  • @Controller defines the class as an HTTP boundary with DI integration
  • Route decorators (@Get, @Post, etc.) declare endpoint paths and methods
  • Parameter decorators (@Body, @Param, @Query) extract and transform request data
  • @UseGuard protects routes with authentication/authorization checks

Decorators are runtime metadata, not build-time transformations. They store information that @nextrush/controllers reads to build optimized route handlers.

Mental Model

Think of decorators as annotations that declare intent:

@Controller('/users')        ← "This class handles /users/* routes"
  └── constructor(UserService) ← "Inject UserService dependency"
  └── @UseGuard(AuthGuard)    ← "Require authentication for all routes"
  └── @Get('/:id')            ← "GET /users/:id calls this method"
      └── @Param('id')        ← "Extract 'id' from route params"

The @nextrush/controllers plugin reads this metadata and builds:

  1. Route registrations with the router
  2. Handler functions that extract parameters
  3. Guard chains that run before handlers
  4. DI resolution for controller instances

Installation

pnpm add @nextrush/decorators

If you use the nextrush meta-package, reflect-metadata is auto-imported. Otherwise, install it separately: pnpm add reflect-metadata and add import 'reflect-metadata' at your entry point.

Required tsconfig.json settings:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Development Runtime

TypeScript decorators require emitDecoratorMetadata to emit runtime type information. Most modern runners (tsx, node --experimental-strip-types) strip types without emitting metadata.

Use @nextrush/dev for development:

pnpm add -D @nextrush/dev
{
  "scripts": {
    "dev": "nextrush dev"
  }
}

| Runtime | Decorator Metadata | Recommended | | ----------------- | ------------------ | -------------- | | nextrush dev | ✅ Full Support | ✅ Development | | tsc + node | ✅ Full Support | ✅ Production | | tsx / esbuild | ❌ Not Supported | ❌ No |

Quick Start

import 'reflect-metadata'; // Required when NOT using the nextrush meta-package
import { Controller, Get, Post, Body, Param, UseGuard } from '@nextrush/decorators';
import type { GuardFn } from '@nextrush/decorators';

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

@UseGuard(AuthGuard)
@Controller('/users')
class UserController {
  constructor(private userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.findAll();
  }

  @Get('/:id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Post()
  create(@Body() data: { name: string; email: string }) {
    return this.userService.create(data);
  }
}

Guard System

Guards determine if a request should proceed to the handler. They run before the route handler and can reject requests.

Function-Based Guards

Simple functions that receive GuardContext and return boolean:

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

const AuthGuard: GuardFn = async (ctx) => {
  const token = ctx.get('authorization');
  if (!token) return false;

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

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

Class-Based Guards (with DI)

Implement CanActivate interface for guards that need dependency injection:

import { Service } from '@nextrush/di';
import type { CanActivate, GuardContext } from '@nextrush/decorators';

@Service()
class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

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

    const user = await this.authService.verify(token);
    ctx.state.user = user;
    return Boolean(user);
  }
}

Applying Guards

// Controller-level (applies to all routes)
@UseGuard(AuthGuard)
@Controller('/admin')
class AdminController {
  @Get()
  dashboard() {} // Protected by AuthGuard
}

// Method-level (applies to specific route)
@Controller('/users')
class UserController {
  @Get()
  findAll() {} // Public

  @UseGuard(AdminGuard)
  @Delete('/:id')
  remove(@Param('id') id: string) {} // Admin only
}

// Multiple guards (all must pass)
@UseGuard(AuthGuard)
@UseGuard(RoleGuard(['admin']))
@Controller('/admin')
class AdminController {}

Guard Execution Order

Guards execute in declaration order: class guards first, then method guards.

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

GuardContext

Guards receive a minimal context (not the full Context) to prevent response manipulation:

interface GuardContext {
  readonly method: string;
  readonly path: string;
  readonly params: Record<string, string>;
  readonly query: Record<string, string | string[] | undefined>;
  readonly headers: Record<string, string | string[] | undefined>;
  readonly body: unknown;
  readonly state: Record<string, unknown>;
  get(name: string): string | undefined;
}

Controller Decorator

@Controller(path?)

Marks a class as an HTTP controller with automatic DI registration.

// With explicit path
@Controller('/users')
class UserController {}

// Auto-derived path (removes 'Controller' suffix, converts to kebab-case)
@Controller()
class UserProfileController {} // → '/user-profile'

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

You do NOT need @Service() when using @Controller()!

// ✅ Correct
@Controller('/users')
class UserController {
  constructor(private userService: UserService) {} // Auto-injected
}

// ❌ Redundant
@Controller('/users')
@Service() // NOT NEEDED
class UserController {}

Route Decorators

@Controller('/users')
class UserController {
  @Get() // GET /users
  findAll() {}

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

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

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

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

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

  @All('/webhook') // All methods
  webhook() {}
}

Route Options

@Get('/search', {
  statusCode: 200,
  description: 'Search users',
  deprecated: false,
})
search() {}

@SetHeader(name, value)

Set response headers on a route. Multiple headers can be applied by stacking decorators.

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

Headers are precomputed at build time and applied before the handler response is sent.

@Redirect(url, statusCode?)

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

@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 an object to override both URL and status code
    return { url: '/new-location', statusCode: 307 };
  }
}

Override behavior:

  • Return void → uses the URL and status code from the decorator
  • Return string → overrides the redirect URL
  • Return { url?, statusCode? } → overrides URL and/or status code

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

Parameter Decorators

@Body(property?, options?)

// Full body
@Post()
create(@Body() data: CreateUserDto) {}

// Specific property
@Post()
create(@Body('name') name: string) {}

// With sync transform
@Post()
create(@Body({ transform: JSON.parse }) data: object) {}

// With async transform (e.g., Zod validation)
@Post()
create(@Body({ transform: UserSchema.parseAsync }) data: User) {}

@Param(name?, options?)

// All params
@Get('/:id/:action')
handle(@Param() params: { id: string; action: string }) {}

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

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

@Query(name?, options?)

// All query params
@Get()
search(@Query() query: Record<string, string>) {}

// Specific param with default
@Get()
paginate(
  @Query('page', { defaultValue: 1, transform: Number }) page: number,
  @Query('limit', { defaultValue: 10, transform: Number }) limit: number
) {}

@Header(name?, options?)

@Get()
handle(
  @Header('authorization') auth: string,
  @Header('x-request-id') requestId?: string
) {}

@Ctx()

Inject the full NextRush Context when you need response methods:

@Get('/:id')
findOne(@Ctx() ctx: Context) {
  const user = this.userService.findOne(ctx.params.id);
  if (!user) {
    ctx.status = 404;
    ctx.json({ error: 'User not found' });
    return;
  }
  ctx.json(user);
}

Transform Functions

Transforms can be sync or async, enabling integration with validation libraries:

import { z } from 'zod';

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

@Controller('/users')
class UserController {
  @Post()
  async create(
    @Body({ transform: CreateUserSchema.parseAsync }) data: z.infer<typeof CreateUserSchema>
  ) {
    // data is validated and typed
    return this.userService.create(data);
  }
}

Custom Parameter Decorators

Create reusable parameter decorators with createCustomParamDecorator(). This is the public API for extending the parameter system.

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

// Extract current user from state (set by auth middleware)
const CurrentUser = createCustomParamDecorator((ctx: Context) => ctx.state.user);

// Extract a specific cookie value
const Cookie = (name: string) =>
  createCustomParamDecorator(
    (ctx: Context) =>
      ctx
        .get('cookie')
        ?.split('; ')
        .find((c) => c.startsWith(`${name}=`))
        ?.split('=')[1]
  );

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

// With transform
const ParsedQuery = (name: string) =>
  createCustomParamDecorator((ctx: Context) => ctx.query[name], { transform: JSON.parse });

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

  @Get('/preferences')
  getPrefs(@Cookie('theme') theme: string, @ApiKey apiKey: string) {
    return { theme, apiKey };
  }
}

createCustomParamDecorator(extractor, options?)

| Parameter | Type | Required | Description | | ----------- | --------------------------- | -------- | ------------------------------------------ | | extractor | (ctx: Context) => T | Yes | Function to extract the value from context | | options | { required?, transform? } | No | Additional options |

Options:

| Option | Type | Default | Description | | ----------- | ------------- | ------- | ------------------------------------------ | | required | boolean | false | Throw MissingParameterError if undefined | | transform | TransformFn | — | Transform the extracted value |

Custom parameter decorators can only be used on method parameters, not constructor parameters.

Metadata Readers

Read decorator metadata programmatically (used internally by @nextrush/controllers):

import {
  isController,
  getControllerMetadata,
  getRouteMetadata,
  getParamMetadata,
  getAllGuards,
  isGuardClass,
} from '@nextrush/decorators';

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

// Get controller path
getControllerMetadata(UserController);
// { path: '/users', version: undefined, ... }

// Get all routes
getRouteMetadata(UserController);
// [{ method: 'GET', path: '/', ... }, ...]

// Get all guards for a route
getAllGuards(UserController, 'findOne');
// [AuthGuard, RoleGuard]

// Check guard type
isGuardClass(AuthGuard); // true for class, false for function

API Reference

Types

// Guard types
type GuardFn = (ctx: GuardContext) => boolean | Promise<boolean>;

interface CanActivate {
  canActivate(ctx: GuardContext): boolean | Promise<boolean>;
}

type Guard = GuardFn | Constructor<CanActivate>;

// Transform type (sync or async)
type TransformFn<TInput = unknown, TOutput = unknown> =
  | ((value: TInput) => TOutput)
  | ((value: TInput) => Promise<TOutput>);

// Metadata types
interface ControllerMetadata {
  path: string;
  version?: string;
  middleware?: MiddlewareRef[];
  tags?: string[];
}

interface RouteMetadata {
  method: RouteMethods;
  path: string;
  methodName: string | symbol;
  propertyKey: string | symbol;
  middleware?: MiddlewareRef[];
  statusCode?: number;
  description?: string;
  deprecated?: boolean;
}

interface ParamMetadata {
  source: ParamSource;
  index: number;
  name?: string;
  required?: boolean;
  defaultValue?: unknown;
  transform?: TransformFn;
  customExtractor?: CustomParamExtractor;
}

type RouteMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
type ParamSource = 'body' | 'query' | 'param' | 'header' | 'ctx' | 'req' | 'res' | 'custom';

// Custom parameter extractor
type CustomParamExtractor<T = unknown> = (ctx: unknown) => T | Promise<T>;

// Response metadata
interface ResponseHeaderMetadata {
  name: string;
  value: string;
}

interface RedirectMetadata {
  url: string;
  statusCode: number;
}

Exports

// Decorators
export { Controller, Get, Post, Put, Patch, Delete, Head, Options, All };
export { Body, Param, Query, Header, Ctx, Req, Res };
export { UseGuard };
export { SetHeader, Redirect };
export { createCustomParamDecorator };

// Types
export type { GuardFn, GuardContext, CanActivate, Guard, Constructor };
export type {
  ControllerMetadata,
  ControllerOptions,
  RouteMetadata,
  RouteOptions,
  ParamMetadata,
  TransformFn,
  CustomParamExtractor,
  ResponseHeaderMetadata,
  RedirectMetadata,
};
export type { BodyOptions, ParamOptions, QueryOptions, HeaderOptions };
export type { GuardMetadata, MiddlewareRef, ParamSource, RouteMethods };
export type { ControllerDefinition };

// Metadata readers
export { isController, getControllerMetadata, getRouteMetadata, getParamMetadata };
export { getAllParamMetadata, getControllerDefinition, buildFullPath };
export { getMethodParameterTypes, getMethodReturnType };
export { getAllGuards, getClassGuards, getMethodGuards, isGuardClass };
export { getResponseHeaders, getRedirectMetadata };

// Constants & type guards
export { DECORATOR_METADATA_KEYS, isValidHttpMethod, isValidParamSource };

Common Mistakes

Mistake 1: Using @Service with @Controller

// ❌ Wrong - redundant
@Controller('/users')
@Service()
class UserController {}

// ✅ Correct - @Controller includes DI
@Controller('/users')
class UserController {}

Mistake 2: Running with tsx

// ❌ Won't work - no decorator metadata
npx tsx src/index.ts

// ✅ Works - full decorator support
nextrush dev

Mistake 3: Forgetting reflect-metadata

// ❌ Wrong - decorators won't work
import { Controller } from '@nextrush/decorators';

// ✅ Correct - must be first import
import 'reflect-metadata';
import { Controller } from '@nextrush/decorators';

Mistake 4: Guards modifying response

// ❌ Wrong - guards shouldn't send responses
const BadGuard: GuardFn = (ctx) => {
  ctx.json({ error: 'Not allowed' }); // GuardContext has no json()!
  return false;
};

// ✅ Correct - just return false, let error handler respond
const GoodGuard: GuardFn = (ctx) => {
  return Boolean(ctx.get('authorization'));
};

License

MIT