Deep Dive into NestJS Decorators: Internals, Usage, and Custom Implementations
While working with NestJS, I was amazed at how extensively it leverages decorators, even though JavaScript doesn't fully support them yet. This made me curious about how NestJS enables decorator support. I then researched how TypeScript implements decorators and whether NestJS follows the same approach or relies on its own custom implementation. In this article, we'll explore exactly that. Introduction NestJS provides a powerful abstraction for building scalable applications, leveraging decorators to simplify and structure code. However, understanding how these decorators work internally can help in optimizing performance, debugging issues, and even creating custom decorators effectively. This article takes a deep dive into: How TypeScript decorators work The current state of JavaScript decorator support How NestJS leverages reflect-metadata to enable powerful decorator functionality How to create and use custom decorators Performance considerations when using decorators 1. Understanding TypeScript Decorators What Are Decorators? Decorators are special functions that modify the behavior of classes, methods, properties, or parameters they are attached to. They are essentially metadata annotations attached using the @ symbol. The Current State of Decorators in JavaScript JavaScript doesn't fully support decorators in its standard yet because: Decorators are still a Stage 3 proposal in ECMAScript (as of March 2025) The proposal has gone through several iterations and design changes Browser implementations are still in progress However, TypeScript has supported decorators as an experimental feature for years, enabling frameworks like NestJS to leverage them. How TypeScript Implements Decorators TypeScript supports decorators through its compiler, which transforms decorator syntax into regular JavaScript function calls. Let's look at a simple example: // Decorator factory - returns the actual decorator function function Log() { // The actual decorator function return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // Replace the original method with a new one that includes logging descriptor.value = function (...args: any[]) { console.log(`Calling ${propertyKey} with`, args); return originalMethod.apply(this, args); }; }; } class User { @Log() getUser(name: string) { return `User: ${name}`; } } const user = new User(); user.getUser("Tejas"); // Logs: "Calling getUser with ["Tejas"]" then returns "User: Tejas" Different Types of Decorators TypeScript supports five types of decorators: Class decorators - Applied to the class constructor Method decorators - Applied to methods Property decorators - Applied to properties Parameter decorators - Applied to function parameters Accessor decorators - Applied to getters/setters Each type of decorator receives different parameters and is used for different purposes in NestJS. What Happens During Compilation? When TypeScript compiles decorators, it transforms them into function calls that wrap around the original code. Here's a simplified version of what the compiled JavaScript might look like for our example: // Simplified compiled output var User = (function () { function User() {} // Original method User.prototype.getUser = function (name) { return "User: " + name; }; // Apply decorator User.prototype.getUser = (function (originalMethod) { return function (...args) { console.log("Calling getUser with", args); return originalMethod.apply(this, args); }; })(User.prototype.getUser); return User; })(); This transformation happens at compile-time, but the decorator's effects occur at runtime. 2. How NestJS Uses Decorators Internally NestJS Decorators & reflect-metadata NestJS extends TypeScript's decorator capabilities using a library called reflect-metadata. This library implements a proposal for adding a Reflection API to JavaScript, allowing frameworks to store and retrieve metadata about classes, methods, and properties. What Exactly is reflect-metadata? reflect-metadata is a polyfill library that implements the Metadata Reflection API proposal. It allows you to: Define metadata on classes and class members Retrieve metadata at runtime Use type information from TypeScript (with some compiler options enabled) This is crucial for NestJS's dependency injection system, which needs to know about types to instantiate them correctly. Example: Using reflect-metadata in NestJS import "reflect-metadata"; // Must be imported once at the application entry point // Create a metadata key const META_KEY = "role"; // Decorator factory function Role(role: string) { //

While working with NestJS, I was amazed at how extensively it leverages decorators, even though JavaScript doesn't fully support them yet. This made me curious about how NestJS enables decorator
support. I then researched how TypeScript implements decorators and whether NestJS follows the same approach or relies on its own custom implementation. In this article, we'll explore exactly that.
Introduction
NestJS provides a powerful abstraction for building scalable applications, leveraging decorators to simplify and structure code. However, understanding how these decorators work internally can help in optimizing performance, debugging issues, and even creating custom decorators effectively.
This article takes a deep dive into:
- How TypeScript decorators work
- The current state of JavaScript decorator support
- How NestJS leverages
reflect-metadata
to enable powerful decorator functionality - How to create and use custom decorators
- Performance considerations when using decorators
1. Understanding TypeScript Decorators
What Are Decorators?
Decorators are special functions that modify the behavior of classes, methods, properties, or parameters they are attached to. They are essentially metadata annotations attached using the
@
symbol.
The Current State of Decorators in JavaScript
JavaScript doesn't fully support decorators in its standard yet because:
- Decorators are still a Stage 3 proposal in ECMAScript (as of March 2025)
- The proposal has gone through several iterations and design changes
- Browser implementations are still in progress
However, TypeScript has supported decorators as an experimental feature for years, enabling frameworks like NestJS to leverage them.
How TypeScript Implements Decorators
TypeScript supports decorators through its compiler, which transforms decorator syntax into regular JavaScript function calls.
Let's look at a simple example:
// Decorator factory - returns the actual decorator function
function Log() {
// The actual decorator function
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// Replace the original method with a new one that includes logging
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return originalMethod.apply(this, args);
};
};
}
class User {
@Log()
getUser(name: string) {
return `User: ${name}`;
}
}
const user = new User();
user.getUser("Tejas"); // Logs: "Calling getUser with ["Tejas"]" then returns "User: Tejas"
Different Types of Decorators
TypeScript supports five types of decorators:
- Class decorators - Applied to the class constructor
- Method decorators - Applied to methods
- Property decorators - Applied to properties
- Parameter decorators - Applied to function parameters
- Accessor decorators - Applied to getters/setters
Each type of decorator receives different parameters and is used for different purposes in NestJS.
What Happens During Compilation?
When TypeScript compiles decorators, it transforms them into function calls that wrap around the original code. Here's a simplified version of what the compiled JavaScript might look like for our example:
// Simplified compiled output
var User = (function () {
function User() {}
// Original method
User.prototype.getUser = function (name) {
return "User: " + name;
};
// Apply decorator
User.prototype.getUser = (function (originalMethod) {
return function (...args) {
console.log("Calling getUser with", args);
return originalMethod.apply(this, args);
};
})(User.prototype.getUser);
return User;
})();
This transformation happens at compile-time, but the decorator's effects occur at runtime.
2. How NestJS Uses Decorators Internally
NestJS Decorators & reflect-metadata
NestJS extends TypeScript's decorator capabilities using a library called reflect-metadata
. This library implements a proposal for adding a Reflection API to JavaScript, allowing frameworks to store and retrieve metadata about classes, methods, and properties.
What Exactly is reflect-metadata
?
reflect-metadata
is a polyfill library that implements the Metadata Reflection API proposal. It allows you to:
- Define metadata on classes and class members
- Retrieve metadata at runtime
- Use type information from TypeScript (with some compiler options enabled)
This is crucial for NestJS's dependency injection system, which needs to know about types to instantiate them correctly.
Example: Using reflect-metadata
in NestJS
import "reflect-metadata"; // Must be imported once at the application entry point
// Create a metadata key
const META_KEY = "role";
// Decorator factory
function Role(role: string) {
// The actual decorator
return function (target: Object, key?: string | symbol) {
// Store metadata on the target
Reflect.defineMetadata(META_KEY, role, target, key);
};
}
class User {
@Role("admin")
accessDashboard() {
// Method implementation
}
}
// Later, retrieve the metadata
const user = new User();
const role = Reflect.getMetadata(META_KEY, User.prototype, "accessDashboard");
console.log(role); // Output: 'admin'
How NestJS Uses reflect-metadata
Under the Hood
NestJS uses reflect-metadata
extensively to implement its core features:
- Dependency Injection: NestJS stores type information to automatically inject dependencies
-
Request Routing:
@Controller()
and@Get()
decorators store routing information -
Guards & Interceptors: Decorators like
@UseGuards()
attach metadata for request handling - Validation: Decorators mark which fields should be validated
3. Deep Dive: How NestJS Executes Decorators Internally
The NestJS Bootstrapping Process
When you start a NestJS application, this happens:
- The application calls
NestFactory.create(AppModule)
- NestJS creates a dependency injection container
- It scans all modules starting from
AppModule
- For each module, it reads metadata from decorators to identify:
- Controllers
- Providers (services, etc.)
- Imports/exports
- It builds a complete dependency graph
Step-by-Step Execution of a Decorator
Let's see how @Controller()
works internally:
- During application development, you decorate a class:
@Controller("users")
export class UserController {
// Controller methods
}
When this code is compiled, TypeScript transforms it, calling the
Controller
functionThe
Controller
function (implemented by NestJS) stores metadata:
// Simplified version of what NestJS does internally
function Controller(prefix?: string): ClassDecorator {
return (target: object) => {
Reflect.defineMetadata("path", prefix || "", target);
Reflect.defineMetadata("__isController", true, target);
};
}
- During application startup, NestJS uses its
DiscoveryService
to scan modules:
// Conceptual example of what happens internally
const isController = Reflect.getMetadata("__isController", UserController);
if (isController) {
const path = Reflect.getMetadata("path", UserController);
// Register the controller with the path 'users'
}
- NestJS creates instances and sets up routing based on this metadata
This is a simplified explanation, but it shows how NestJS uses decorators and metadata to build its entire system.
Real-World Example: How @Injectable()
Works
The @Injectable()
decorator is core to NestJS's dependency injection. Here's how it works:
// Your code
@Injectable()
export class UserService {
constructor(private databaseService: DatabaseService) {}
}
// What NestJS does internally (simplified)
function Injectable(): ClassDecorator {
return (target: object) => {
Reflect.defineMetadata("__injectable", true, target);
// Get parameter types from TypeScript
const types = Reflect.getMetadata("design:paramtypes", target) || [];
// Store dependency types for later injection
Reflect.defineMetadata("__paramtypes", types, target);
};
}
// During application startup, NestJS creates instances
const paramTypes = Reflect.getMetadata("__paramtypes", UserService);
// paramTypes would be [DatabaseService]
// NestJS would then:
// 1. Create/find an instance of DatabaseService
// 2. Create UserService passing in the DatabaseService instance
const databaseService = container.get(DatabaseService);
const userService = new UserService(databaseService);
4. Custom Decorators in NestJS
Creating custom decorators in NestJS is surprisingly easy because NestJS provides utilities to simplify the process.
Example: Creating a @Permissions
Decorator
import { SetMetadata } from "@nestjs/common";
// NestJS provides SetMetadata to make decorator creation easier
export const Permissions = (...permissions: string[]) => SetMetadata("permissions", permissions);
Using the Decorator in a Controller
import { Controller, Get } from "@nestjs/common";
import { Permissions } from "./permissions.decorator";
@Controller("users")
export class UserController {
@Get()
@Permissions("read:users", "write:users")
getUsers() {
return "Users List";
}
}
Retrieving Metadata in a Guard
NestJS provides the Reflector
class to simplify metadata retrieval:
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Get the permissions required for this endpoint
const requiredPermissions = this.reflector.get<string[]>("permissions", context.getHandler());
if (!requiredPermissions) {
return true; // No permissions required
}
const request = context.switchToHttp().getRequest();
const user = request.user;
// Check if user has all required permissions
return requiredPermissions.every((permission) => user?.permissions?.includes(permission));
}
}
Advanced: Custom Parameter Decorators
NestJS also supports custom parameter decorators, which can extract and transform data from requests:
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
// Create a decorator that extracts a specific user property
export const UserProperty = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// data is the property name passed to the decorator
return data ? user?.[data] : user;
});
// Usage in a controller
@Controller("users")
export class UserController {
@Get("profile")
getProfile(@UserProperty("id") userId: string) {
return `User ID: ${userId}`;
}
}
Composing Multiple Decorators
One powerful feature of decorators is that you can combine them:
import { applyDecorators, Get, UseGuards } from "@nestjs/common";
import { PermissionsGuard } from "./permissions.guard";
import { Permissions } from "./permissions.decorator";
// Create a combined decorator
export function SecureEndpoint(path: string, ...permissions: string[]) {
return applyDecorators(Get(path), Permissions(...permissions), UseGuards(PermissionsGuard));
}
// Usage
@Controller("users")
export class UserController {
@SecureEndpoint("admin", "read:users", "manage:users")
getAdminDashboard() {
return "Admin Dashboard";
}
}
This pattern is exactly how NestJS creates many of its built-in decorators!
5. Debugging Decorators in NestJS
Decorators can sometimes be tricky to debug, so here are some tips:
Common Decorator Problems and Solutions
Problem: Metadata Not Being Applied
// This might not work as expected
@Controller("users")
class UserController {}
Solution: Make sure you're exporting the class:
@Controller("users")
export class UserController {}
Problem: Decorator Order Matters
// This might produce unexpected results
@UseGuards(AuthGuard)
@Permissions('admin')
@Get()
method() {}
Solution: Remember that decorators are applied bottom-to-top, so reorder them:
@Get()
@Permissions('admin')
@UseGuards(AuthGuard)
method() {}
Problem: Missing reflect-metadata
If you get errors about metadata, make sure:
// At your application entry point
import "reflect-metadata";
Inspecting Metadata at Runtime
You can debug metadata by adding:
console.log(Reflect.getMetadataKeys(target));
console.log(Reflect.getMetadata("key", target));
6. Performance Considerations with NestJS Decorators
While decorators are powerful, they do have performance implications:
Benchmarking Decorator Overhead
In my testing, I found that heavy use of decorators and metadata can add approximately 5-10% overhead to request processing times, though this varies widely based on usage.
Optimization Strategies
- Cache Metadata Lookups: If you're repeatedly accessing the same metadata, store it in a variable
// Instead of this in a hot path
const permissions = this.reflector.get('permissions', handler);
// Do this
private readonly handlerPermissionsMap = new Map();
getPermissions(handler) {
if (!this.handlerPermissionsMap.has(handler)) {
const permissions = this.reflector.get('permissions', handler);
this.handlerPermissionsMap.set(handler, permissions);
}
return this.handlerPermissionsMap.get(handler);
}
Minimize Decorator Count: Don't overuse decorators; each one adds processing time
Lazy Evaluation: If your decorator performs expensive operations, do them lazily:
function ExpensiveOperation() {
let result;
return (target, key, descriptor) => {
const original = descriptor.value;
descriptor.value = function(...args) {
// Only calculate once
if (!result) {
result = /* expensive calculation */;
}
return original.apply(this, [...args, result]);
};
};
}
Conclusion
NestJS decorators simplify development by abstracting complex logic into reusable annotations. Understanding their internals provides better debugging, optimization, and customization capabilities.
- TypeScript decorators transform class behavior at runtime through compiler transformations
-
NestJS enhances decorators using
reflect-metadata
to implement its dependency injection system - Custom decorators allow developers to extend NestJS functionality efficiently
- Debugging and optimizing decorators requires understanding their execution flow
With this knowledge, you can now optimize, debug, and even build advanced decorators in NestJS like a pro!