Modern backend frameworks emphasize modularity, scalability, and maintainability — and Dependency Injection (DI) plays a key role in achieving these goals. In the world of Node.js, NestJS stands out as a progressive framework built with TypeScript, drawing inspiration from Angular to provide a highly structured and opinionated architecture. One of the pillars of NestJS is its built-in Dependency Injection system.
Dependency Injection is a design pattern that allows a class to receive its dependencies from an external source rather than creating them itself. This makes code more modular, easier to test, and better aligned with the SOLID principles, especially the Dependency Inversion Principle.
In NestJS, DI is not just a feature — it's the foundation of how services, controllers, and modules interact. Whether you're building a simple API or a large-scale enterprise application, understanding how DI works in NestJS is essential for writing clean and maintainable code.
In this tutorial, we'll explore:
-
What Dependency Injection is and how it works in NestJS
-
How to define and inject services and providers
-
Different types of providers and use cases
-
Best practices and common pitfalls
By the end, you'll gain a solid understanding of how to leverage DI effectively in your NestJS applications.
What is Dependency Injection in NestJS?
Dependency Injection (DI) is a design pattern where dependencies — typically services or other classes — are provided to a class rather than the class creating them itself. This promotes loose coupling, making your code easier to maintain, test, and scale.
In NestJS, DI is a first-class citizen powered by TypeScript’s decorators and metadata reflection using the reflect-metadata
package. The framework comes with a powerful built-in Inversion of Control (IoC) container that automatically manages the creation and lifecycle of providers.
Key Concepts in NestJS DI:
✅ Providers
In NestJS, anything that can be injected is referred to as a provider. This includes services, factories, repositories, or any class marked with the @Injectable()
decorator.
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoggerService {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
}
✅ Injecting Providers
Providers can be injected into other classes via constructor injection. For example, injecting the LoggerService
into an AppService
:
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Injectable()
export class AppService {
constructor(private readonly logger: LoggerService) {}
getHello(): string {
this.logger.log('getHello() was called');
return 'Hello World!';
}
}
✅ Modules and Registration
For DI to work, the provider must be registered in a module:
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { LoggerService } from './logger.service';
@Module({
providers: [AppService, LoggerService],
})
export class AppModule {}
NestJS handles the instantiation and lifecycle of these providers. When AppService
is created, Nest automatically resolves and injects an instance of LoggerService
.
Why This Matters
This mechanism:
-
Reduces manual instantiation of services
-
Improves testability through mocks and stubs
-
Encourages modular design by encapsulating functionality in providers
In the next section, we’ll put this into practice by creating a simple service and injecting it into a controller.
Creating a Simple Service with Dependency Injection
To see Dependency Injection in action, let’s create a simple NestJS application with a custom service that gets injected into a controller.
📦 Step 1: Create a New NestJS Project
If you haven’t already, install the Nest CLI and create a new project:
npm i -g @nestjs/cli
nest new nest-di-example
Choose a package manager (e.g., npm or yarn), and once it’s done, navigate to the project directory:
cd nest-di-example
🛠️ Step 2: Generate a Service
Let’s generate a LoggerService
that will be injected into the AppController.
nest generate service logger
This will create a file src/logger/logger.service.ts
with the following boilerplate:
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoggerService {
log(message: string) {
console.log(`[LoggerService]: ${message}`);
}
}
🧪 Step 3: Use the Service in a Controller
Open src/app.controller.ts
and modify it to inject and use the LoggerService
.
import { Controller, Get } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
@Controller()
export class AppController {
constructor(private readonly logger: LoggerService) {}
@Get()
getHello(): string {
this.logger.log('GET / endpoint was hit');
return 'Hello from NestJS with DI!';
}
}
📦 Step 4: Register the Service in a Module
Open src/app.module.ts
and ensure the LoggerService
is included in the providers
array:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerService } from './logger/logger.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, LoggerService],
})
export class AppModule {}
✅ NestJS will automatically inject LoggerService
into any class that requests it, thanks to the @Injectable()
decorator and the module system.
🚀 Step 5: Run the Application
Start the development server:
npm run start
Visit http://localhost:3000 and check your terminal. You should see:
[LoggerService]: GET / endpoint was hit
This simple example shows how easy it is to leverage Dependency Injection in NestJS. Next, we’ll take it a step further by exploring custom providers and the different strategies available (useClass
, useValue
, useFactory
, useExisting
).
Using Custom Providers in NestJS
In NestJS, a provider is any class or value that can be injected as a dependency. While most of the time we use classes with the @Injectable()
decorator, sometimes we need more flexibility. NestJS allows us to define custom providers using different strategies like useClass
, useValue
, useFactory
, and useExisting
.
These strategies give you full control over how and what is injected, making them essential in real-world applications for injecting:
-
External libraries
-
Configuration values
-
Mocked data in tests
-
Conditional services
🧩 1. useClass
– Specify Which Class to Instantiate
You can override which class to use when injecting a dependency.
interface Logger {
log(message: string): void;
}
@Injectable()
export class FileLoggerService implements Logger {
log(message: string) {
console.log(`[FileLogger]: ${message}`);
}
}
@Injectable()
export class ConsoleLoggerService implements Logger {
log(message: string) {
console.log(`[ConsoleLogger]: ${message}`);
}
}
Register with a custom token:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConsoleLoggerService } from './logger/logger.service';
import { LoggerModule } from './logger/logger.module';
@Module({
imports: [LoggerModule],
controllers: [AppController],
providers: [
AppService,
{
provide: 'LoggerService',
useClass: ConsoleLoggerService,
},
],
})
export class AppModule { }
Inject using the @Inject()
decorator:
import { Inject } from '@nestjs/common';
@Injectable()
export class AppService {
constructor(@Inject('LoggerService') private readonly logger: Logger) {}
getHello(): string {
this.logger.log('Hello from custom useClass provider');
return 'Hello!';
}
}
🧱 2. useValue
– Inject a Static Value or Object
Create a config.ts interface.
export interface AppConfig {
apiKey: string;
timeout: number;
}
export const config: AppConfig = {
apiKey: '123456',
timeout: 3000,
};
Use this when you want to inject a constant or configuration object.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConsoleLoggerService } from './logger/logger.service';
import { LoggerModule } from './logger/logger.module';
import { ApiService } from './api/api.service';
import { config } from './config';
@Module({
imports: [LoggerModule],
controllers: [AppController],
providers: [
AppService,
{
provide: 'LoggerService',
useClass: ConsoleLoggerService,
},
{
provide: 'CONFIG',
useValue: config,
},
ApiService,
],
})
export class AppModule { }
Inject it like this:
import { Inject, Injectable } from '@nestjs/common';
import { AppConfig } from 'src/config';
@Injectable()
export class ApiService {
constructor(@Inject('CONFIG') private readonly config: AppConfig) { }
callApi() {
console.log(`Using API key: ${this.config.apiKey}`);
}
}
⚙️ 3. useFactory
– Dynamic/Asynchronous Provider
Useful when your provider needs to be generated at runtime, e.g., based on environment variables or async operations.
@Module({
providers: [
{
provide: 'CONFIG',
useFactory: () => {
return {
apiKey: process.env.API_KEY || 'default-key',
timeout: 5000,
};
},
},
],
})
export class AppModule {}
Inject it:
@Injectable()
export class DbService {
constructor(@Inject('DATABASE_CONNECTION') private readonly connection: string) {}
connect() {
console.log(this.connection);
}
}
You can also use async
functions with useFactory
if needed, and even inject other providers into the factory.
♻️ 4. useExisting
– Alias to Another Provider
This allows one provider token to reference another.
@Module({
providers: [
ConsoleLoggerService,
{
provide: 'LoggerService',
useExisting: ConsoleLoggerService,
},
],
})
export class AppModule {}
🧠 Best Practice Tip
When using string tokens (like 'LoggerService'
or 'CONFIG'
), use TypeScript Symbols or constants to avoid typos:
export const LOGGER = Symbol('LOGGER');
export const CONFIG = Symbol('CONFIG');
Then:
providers: [
{
provide: LOGGER,
useClass: ConsoleLoggerService,
},
];
Provider Scope in NestJS: Singleton, Request, and Transient
In NestJS, all providers are singleton-scoped by default. However, you can customize their lifecycle to better fit your use case, especially useful for stateful services, multi-tenant apps, or request-specific logic.
NestJS supports three scopes:
Scope | Description |
---|---|
DEFAULT (Singleton) |
A single instance shared across the entire application |
REQUEST |
A new instance created per incoming request |
TRANSIENT |
A new instance created every time it's injected |
1️⃣ Singleton (Default)
-
No need to explicitly set a scope.
-
One instance of the provider is created and shared across the app.
@Injectable()
export class LoggerService {
private counter = 0;
log(message: string) {
this.counter++;
console.log(`[${this.counter}]: ${message}`);
}
}
Used in multiple controllers/services — all share the same LoggerService
instance.
2️⃣ Request Scoped Providers
A new instance is created for each HTTP request. Use this for request-specific state, such as storing authenticated user info.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {
private timestamp = Date.now();
log(message: string) {
console.log(`[Request @${this.timestamp}]: ${message}`);
}
}
⚠️ When using request scope:
-
All dependent services must also be
REQUEST
scoped, or Nest will throw an error.
3️⃣ Transient Scoped Providers
Every time the service is injected (even within the same request), a new instance is created.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class TransientLoggerService {
private id = Math.random();
log(message: string) {
console.log(`[Transient ${this.id}]: ${message}`);
}
}
You can inject this into a controller or service like this:
@Controller()
export class AppController {
constructor(private readonly logger: TransientLoggerService) {}
@Get()
getHello() {
this.logger.log('Handling GET /');
return 'Hello!';
}
}
Every injection results in a new instance, so even two calls inside the same controller will have different id
s.
🧪 Which Scope Should You Use?
Use Case | Scope |
---|---|
Stateless services (logging, DB access) | Singleton (default) |
Per-request state (current user, context) | Request |
Truly short-lived / throwaway instances | Transient |
🧠 Gotchas & Best Practices
-
✅ Prefer singleton unless you explicitly need request-specific state.
-
⚠️ Avoid injecting
REQUEST
orTRANSIENT
scoped providers into singleton ones — it will throw a RuntimeException. -
✅ If you need mixed scopes, consider injecting via factory providers.
Asynchronous Providers in NestJS (Real-World Example: Database)
NestJS allows you to create asynchronous providers using useFactory
. This is especially useful when you want to:
-
Initialize a database connection
-
Load configuration from an async source
-
Inject values after async setup
🔌 Example: Async MongoDB Connection Provider
Let’s walk through how to register a MongoDB connection using mongodb
package and an async factory.
1️⃣ Install MongoDB Client
npm install mongodb
2️⃣ Create an Async Provider for MongoDB
// database.providers.ts
import { MongoClient, Db } from 'mongodb';
export const DatabaseProvider = {
provide: 'DATABASE_CONNECTION',
useFactory: async (): Promise<Db> => {
const uri = process.env.MONGO_URI || 'mongodb://localhost:27017';
const client = new MongoClient(uri);
await client.connect();
return client.db('nest_di_demo');
},
};
☝️ This
useFactory
is asynchronous (async () => ...
) and returns a connectedDb
instance.
3️⃣ Register the Provider in a Module
// database.module.ts
import { Module } from '@nestjs/common';
import { DatabaseProvider } from './database.providers';
@Module({
providers: [DatabaseProvider],
exports: [DatabaseProvider], // export for other modules
})
export class DatabaseModule {}
4️⃣ Inject the Database in a Service
// user.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { Db } from 'mongodb';
@Injectable()
export class UserService {
constructor(@Inject('DATABASE_CONNECTION') private readonly db: Db) {}
async findAllUsers(): Promise<any[]> {
return this.db.collection('users').find().toArray();
}
}
5️⃣ Register UserService
and Use It
// user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
Then inject UserService
into any controller or other service.
🧠 Bonus: Inject Other Providers into the Factory
Async factories can also receive dependencies:
{
provide: 'SOME_PROVIDER',
useFactory: async (configService: ConfigService) => {
const value = await configService.getAsync('someKey');
return new SomeDependency(value);
},
inject: [ConfigService], // 👈 dependencies here
}
🔐 Best Practices
-
Always export async providers if needed in other modules.
-
Use constants or symbols as tokens instead of plain strings (e.g.,
DATABASE_CONNECTION
). -
Centralize config and secret handling using
@nestjs/config
.
Interface-Based Dependency Injection and Injection Tokens in NestJS
TypeScript interfaces are extremely useful for defining contracts, but they don’t exist at runtime, which creates a challenge when using them with NestJS's DI system (which depends on runtime metadata). To bridge this, NestJS provides a way to use custom injection tokens for injecting providers that adhere to an interface.
🤔 The Problem
Let’s say you define an interface:
export interface Logger {
log(message: string): void;
}
And then create a class that implements it:
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';
@Injectable()
export class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[Console]: ${message}`);
}
}
Now, if you try this:
constructor(private readonly logger: Logger) {} // ❌ Won’t work
You'll get an error because Logger
is erased at runtime — TypeScript interfaces do not exist in JavaScript after transpilation.
✅ The Solution: Use Injection Tokens
To use interfaces in DI, you must:
-
Create a token (string or symbol)
-
Register the provider using
provide: TOKEN
-
Inject using
@Inject(TOKEN)
🛠 Step-by-Step Example
1️⃣ Create an Interface and Implementation
// logger.interface.ts
export interface Logger {
log(message: string): void;
}
// console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';
@Injectable()
export class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[ConsoleLogger]: ${message}`);
}
}
2️⃣ Create a Symbol Token
// logger.constants.ts
export const LOGGER = Symbol('LOGGER');
3️⃣ Register the Provider with the Token
// app.module.ts
import { Module } from '@nestjs/common';
import { ConsoleLogger } from './console-logger.service';
import { LOGGER } from './logger.constants';
@Module({
providers: [
{
provide: LOGGER,
useClass: ConsoleLogger,
},
],
exports: [LOGGER],
})
export class AppModule {}
4️⃣ Inject Using @Inject(TOKEN)
// app.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';
import { LOGGER } from './logger.constants';
@Injectable()
export class AppService {
constructor(@Inject(LOGGER) private readonly logger: Logger) {}
getHello(): string {
this.logger.log('Hello from AppService');
return 'Hello!';
}
}
✅ This works because you're bypassing the erased interface by using a runtime-safe token.
🧠 Why Use This Pattern?
-
Decouples your app from specific implementations
-
Makes it easy to swap out implementations (e.g.,
ConsoleLogger
→FileLogger
) -
Enables more testable and flexible architecture (e.g., mocking logger in tests)
✅ Token Best Practices
Token Type | Recommended Usage |
---|---|
string |
Quick and simple but prone to typos |
symbol |
Safer, avoids naming collisions |
class |
Can be used directly unless abstracted by interface |
Common Dependency Injection Pitfalls and Best Practices
While NestJS’s Dependency Injection system is powerful and intuitive, there are a few common pitfalls developers encounter, especially when working on large-scale applications. Let’s go over them along with best practices to avoid them.
⚠️ Common Pitfalls
1️⃣ Forgetting to Register Providers
If you see an error like Nest can't resolve dependencies of the X
, it's usually because a provider wasn’t registered in its module.
✅ Fix: Always register custom providers in the
providers
array of the module — or import the module where it’s defined.
2️⃣ Injecting Interfaces Without Tokens
TypeScript interfaces do not exist at runtime. Trying to inject private readonly logger: Logger
will fail unless you use an injection token.
✅ Fix: Use custom tokens (
string
,symbol
, or class) and@Inject(TOKEN)
when injecting something that implements an interface.
3️⃣ Incorrect Provider Scopes
Injecting a REQUEST
or TRANSIENT
scoped provider into a singleton causes a runtime error due to incompatible lifecycles.
✅ Fix: If you use request-scoped providers, ensure any class injecting them is also request-scoped (same for transient).
4️⃣ Overusing Transient Scope
Using Scope.TRANSIENT
everywhere can lead to performance issues due to excessive instantiation.
✅ Fix: Stick to the default singleton unless you really need a new instance on every injection.
5️⃣ Circular Dependencies
A circular dependency happens when two or more providers depend on each other, directly or indirectly.
✅ Fix: Refactor your logic to break the cycle, or use
forwardRef(() => SomeService)
as a last resort.
✅ Best Practices
-
Use Interfaces + Tokens: Especially for services that may be swapped or mocked.
-
Modularize Your App: Group related services into feature modules and export only what’s necessary.
-
Use Async Factories Wisely: Great for dynamic providers (like DB or config), but don’t overcomplicate basic DI.
-
Leverage Scope Properly: Know when to use
Singleton
,Request
, orTransient
scopes. -
Prefer
useClass
over direct injection for abstracted logic — improves testability and flexibility. -
Use constants or symbols for injection tokens to avoid string typos and collisions.
Conclusion
Dependency Injection is at the heart of NestJS, enabling clean, modular, and testable code. By understanding how NestJS manages providers, scopes, and tokens, you can build powerful backend applications that scale easily and remain maintainable.
Whether you're building a REST API, a GraphQL service, or a microservice architecture, mastering DI will help you write elegant, decoupled code that follows best practices by design.
✅ With the concepts and patterns covered in this tutorial, you’re now ready to confidently use DI in your NestJS projects!
You can get the full source code from our GitHub.
That's just the basics. If you need more deep learning about Nest.js, you can take the following cheap course:
-
Learn NestJS from Scratch
-
Mastering NestJS
-
Angular & NestJS Full Stack Web Development Bootcamp 2023
-
React and NestJS: A Practical Guide with Docker
-
NestJS Masterclass - NodeJS Framework Backend Development
-
Build A TodoList with NestJS and Vue JS
-
The Complete Angular & NestJS Course
Thanks!