How to Use Decorators for Dependency Injection in NestJS

NestJS uses decorators like @Injectable() and @Inject() to attach metadata that powers its metadata-driven Dependency Injection container, enabling automatic provider registration, token resolution, and scope management.

The nestjs/nest framework implements a sophisticated metadata-driven Dependency Injection (DI) container that automatically discovers providers and resolves dependencies. Understanding how to use decorators for dependency injection is essential for building scalable, testable applications with NestJS.

Core Decorators for Dependency Injection

NestJS provides two primary decorators that drive the DI system: @Injectable() for registering providers and @Inject() for declaring specific dependencies.

The @Injectable() Decorator

The @Injectable() decorator marks a class as a provider so the DI container can register it. According to the source code in packages/common/decorators/core/injectable.decorator.ts, this decorator writes two critical metadata keys using the Reflect API:

  • INJECTABLE_WATERMARK – a boolean flag that tells the scanner this class is a provider
  • SCOPE_OPTIONS_METADATA – stores optional scope information (e.g., Scope.REQUEST, Scope.TRANSIENT)
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestIdService {
  private readonly id = Math.random().toString(36).substring(7);
  getId() {
    return this.id;
  }
}

The @Inject() Decorator

The @Inject() decorator declares a dependency for a constructor parameter or a class property. As implemented in packages/common/decorators/core/inject.decorator.ts, it allows you to override the default token (the class type) with a custom token.

When used on constructor parameters, it writes to SELF_DECLARED_DEPS_METADATA. When used on properties, it writes to PROPERTY_DEPS_METADATA.

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class LoggerService {
  @Inject('CONFIG')
  private readonly config!: ConfigService;
}

How Metadata Powers the DI Container

NestJS uses the Reflect API to store metadata that drives the entire injection lifecycle. The constants defined in packages/common/constants.ts serve as the keys for this metadata storage.

Key Metadata Keys

  • INJECTABLE_WATERMARK – Identifies classes that should be treated as providers
  • SCOPE_OPTIONS_METADATA – Stores scope configuration for instance lifecycle management
  • SELF_DECLARED_DEPS_METADATA – Array of { index, param } objects for constructor parameter overrides
  • PROPERTY_DEPS_METADATA – Array of { key, type } objects for property injection targets
  • PARAMTYPES_METADATA – Stores design-time type information for constructor parameters

Module Scanning and Resolution

During the module scanning phase, packages/core/scanner.ts checks these metadata keys to:

  1. Register providers – Classes with INJECTABLE_WATERMARK are added to the module's provider list via modulesContainer.addInjectable
  2. Build dependency graphs – The scanner reads SELF_DECLARED_DEPS_METADATA and PARAMTYPES_METADATA to map dependencies
  3. Handle scope resolution – packages/core/injector/instance-loader.ts reads SCOPE_OPTIONS_METADATA to create singleton, request-scoped, or transient instances

Practical Implementation Examples

Basic Provider Registration

The simplest use case marks a class as injectable without custom tokens:

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  findAll() {
    return ['tabby', 'siamese'];
  }
}

The @Injectable() decorator registers CatsService as a provider. The class name itself serves as the injection token.

Constructor Injection with Automatic Token Resolution

When constructor parameters are not explicitly decorated with @Inject, Nest reads the design-type metadata (design:paramtypes) automatically:

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  getAll() {
    return this.catsService.findAll();
  }
}

Nest matches the CatsService type in the constructor to the registered provider.

Property Injection with @Inject

For cases where constructor injection is not suitable, use property injection:

import { Injectable, Inject } from '@nestjs/common';
import { ConfigService } from './config.service';

@Injectable()
export class LoggerService {
  @Inject('CONFIG')
  private readonly config!: ConfigService;
}

The @Inject('CONFIG') decorator stores this dependency in PROPERTY_DEPS_METADATA, and the DI container sets the property after instantiation.

Custom Providers and Injection Tokens

When using string or symbol tokens, you must define custom providers:

// config.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({
  providers: [
    {
      provide: 'CONFIG',
      useClass: ConfigService,
    },
  ],
  exports: ['CONFIG'],
})
export class ConfigModule {}
// app.service.ts
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class AppService {
  constructor(@Inject('CONFIG') private readonly configService: ConfigService) {}
}

The custom provider associates the string token 'CONFIG' with ConfigService. The @Inject('CONFIG') decorator tells the container which token to resolve.

Request-Scoped Providers

Control instance lifetime using scope options:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestIdService {
  private readonly id = Math.random().toString(36).substring(7);
  getId() {
    return this.id;
  }
}

Because the scope option is stored in SCOPE_OPTIONS_METADATA, the instance-loader.ts creates a new instance for each incoming request when the scope is REQUEST, or a fresh instance each injection when TRANSIENT.

Summary

  • @Injectable() marks classes as providers by setting INJECTABLE_WATERMARK and SCOPE_OPTIONS_METADATA metadata, enabling the DI container to register and scope them.
  • @Inject(token) explicitly declares dependencies for constructor parameters or properties, storing metadata in SELF_DECLARED_DEPS_METADATA or PROPERTY_DEPS_METADATA.
  • Automatic resolution occurs when Nest reads design:paramtypes metadata for undecorated constructor parameters, matching them to registered providers by type.
  • Custom tokens (strings, symbols, or forward references) require explicit @Inject() usage and corresponding custom provider definitions in module metadata.
  • Scope management is handled through SCOPE_OPTIONS_METADATA, allowing singleton (default), request-scoped, or transient provider lifecycles.

Frequently Asked Questions

What is the difference between @Injectable and @Inject?

@Injectable() is a class-level decorator that marks a class as a provider so the NestJS DI container can register it, whereas @Inject() is a parameter or property-level decorator that explicitly specifies which dependency to inject when automatic type resolution is insufficient or when using custom tokens. According to the source code in packages/common/decorators/core/injectable.decorator.ts, @Injectable() writes INJECTABLE_WATERMARK metadata, while @Inject() in packages/common/decorators/core/inject.decorator.ts writes to SELF_DECLARED_DEPS_METADATA or PROPERTY_DEPS_METADATA.

How does NestJS resolve dependencies without the @Inject decorator?

When constructor parameters lack the @Inject() decorator, NestJS reads the design:paramtypes metadata automatically emitted by TypeScript's compiler. The scanner in packages/core/scanner.ts checks PARAMTYPES_METADATA to determine the design-time types of constructor arguments. If a provider is registered with @Injectable() and its class matches the parameter type, the container injects that instance directly. This automatic resolution only works when the token is the class type itself, not for string tokens or interfaces.

Can I use property injection instead of constructor injection in NestJS?

Yes, NestJS supports property injection using the @Inject() decorator on class properties. When applied to a property, the decorator stores metadata in PROPERTY_DEPS_METADATA (as defined in packages/common/constants.ts), and the injector in packages/core/injector/instance-loader.ts sets the property value after class instantiation. While property injection works for circular dependencies or optional dependencies, constructor injection is generally preferred because it makes dependencies explicit and facilitates unit testing.

What are the performance implications of request-scoped providers?

Request-scoped providers, configured via @Injectable({ scope: Scope.REQUEST }), create a new instance for every incoming HTTP request, which increases memory usage and garbage collection pressure compared to singleton providers. According to the implementation in packages/core/injector/instance-loader.ts, the container checks SCOPE_OPTIONS_METADATA to determine if it should create a new instance or reuse an existing one. While this enables request-specific state management (like request IDs or user contexts), you should limit request-scoped providers to necessary cases to avoid performance bottlenecks in high-throughput applications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →