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) { //

Mar 23, 2025 - 09:47
 0
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:

  1. Class decorators - Applied to the class constructor
  2. Method decorators - Applied to methods
  3. Property decorators - Applied to properties
  4. Parameter decorators - Applied to function parameters
  5. 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:

  1. Dependency Injection: NestJS stores type information to automatically inject dependencies
  2. Request Routing: @Controller() and @Get() decorators store routing information
  3. Guards & Interceptors: Decorators like @UseGuards() attach metadata for request handling
  4. 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:

  1. The application calls NestFactory.create(AppModule)
  2. NestJS creates a dependency injection container
  3. It scans all modules starting from AppModule
  4. For each module, it reads metadata from decorators to identify:
    • Controllers
    • Providers (services, etc.)
    • Imports/exports
  5. It builds a complete dependency graph

Step-by-Step Execution of a Decorator

Let's see how @Controller() works internally:

  1. During application development, you decorate a class:
   @Controller("users")
   export class UserController {
    // Controller methods
   }
  1. When this code is compiled, TypeScript transforms it, calling the Controller function

  2. The 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);
    };
   }
  1. 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'
   }
  1. 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!