What are providers and how does dependency injection work in NestJS?
A NestJS provider is any class the Nest IoC container builds and injects for you, instead of you writing new by hand. Services, repositories, factories, even plain config values - all of them can be providers. The container reads constructor parameter types through reflect-metadata at bootstrap, finds the matching entry in the module's providers array, and passes the cached instance in before any request reaches your controller.
Theory
TL;DR
- Providers let the container handle object creation; controllers declare what they need, not how to build it
- Analogy: the IoC container is a kitchen that takes orders (constructor parameters) and delivers assembled dishes (instances) - controllers never touch the ingredients
- Main difference:
new UsersService()ties classes together; a provider decouples them so swapping one service updates every consumer automatically - Three requirements:
@Injectable()on the class, the class in the module'sprovidersarray, andemitDecoratorMetadata: trueintsconfig.json - Default scope is singleton per application; opt into
Scope.REQUESTorScope.TRANSIENTwhen you need per-request or per-injection instances
Quick example
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // marks this class as a container-managed provider
export class UsersService {
findAll() {
return [{ id: 1, name: 'Alice' }];
}
}
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {} // container injects here
@Get()
findAll() {
return this.users.findAll(); // no new UsersService() anywhere
}
}
// users.module.ts
@Module({
controllers: [UsersController],
providers: [UsersService], // register with the container
})
export class UsersModule {}The controller never calls new. The module registers UsersService as a recipe, the container builds it once at bootstrap, and injects the cached instance into any constructor that declares the type.
Why new breaks
Imagine UsersController calling new UsersService() directly. Works once. Then you need to mock the service in tests, share one instance across two controllers, or swap it for a MockUsersService on staging, and every place that wrote new UsersService() has to change at the same time.
Dependency injection (DI) fixes this. You declare what you need, the container decides how to deliver it. In Nest, that container manages classes called providers. This is the answer the interviewer wants at a junior NestJS interview - not "a class with @Injectable", but an explanation of why new fails and what the container does instead.
How the container resolves constructor types
When you write constructor(private users: UsersService), the TypeScript compiler stores the parameter types in compiled output that reflect-metadata can read. Nest reads that metadata array at module load, looks up each type in the module's providers registry, and builds the dependency graph from the bottom up, leaf dependencies first.
Three things must be true simultaneously for injection to work:
- The class has
@Injectable() - The class is in the
providersarray of a module the app sees tsconfig.jsonhas bothemitDecoratorMetadataandexperimentalDecoratorsset totrue
Break any one of these and you get "Nest can't resolve dependencies of X" at startup.
What happens at bootstrap
Here is the exact sequence every Nest app goes through when NestFactory.create() runs:
- Nest scans every
@Module()and collects itsproviders,controllers, andimports - For each provider, Nest reads the constructor parameter types via
reflect-metadata - Nest builds a directed acyclic graph of providers and resolves deeper dependencies first
- Nest constructs one instance per provider in the right order and caches it by token
- When an HTTP request arrives, the cached controller already holds the cached service, so the handler runs with no additional wiring
Step 3 is where circular dependencies break things. If UsersService needs AuthService and AuthService needs UsersService, there is a cycle and the container cannot decide which to build first. The fix is forwardRef(() => OtherService) on both sides, which tells the container to resolve the reference lazily.
When to use providers
- Shared business logic - a
UsersServiceused by multiple controllers belongs as a provider - Testability - inject a mock via
useClassoruseValuewithout touching source code - External libraries - wrap a database client in a provider with
useFactoryfor async initialization - Config values -
useValueoruseFactoryproviders for environment-driven settings - One-off calculation - a local function is fine; no need to register a provider for something used in exactly one place
Custom providers: useValue, useClass, useFactory, useExisting
Nine out of ten providers are just classes. The tenth one needs a custom provider - a small object that tells the container how to build the value rather than having it call new directly. Knowing when to pick which form comes up as a follow-up in senior interviews.
// useValue: hand the container an already-constructed value
{ provide: 'APP_CONFIG', useValue: { maxRetries: 3, region: 'eu' } }
// useClass: swap one class for another with the same shape
{ provide: MailerService, useClass: MockMailerService }
// useFactory: run code at bootstrap (can be async) to produce the instance
{
provide: 'DB_CONNECTION',
useFactory: async (config: ConfigService) => createPool(config.get('DB_URL')),
inject: [ConfigService], // container resolves this before running the factory
}
// useExisting: alias one provider token to another already registered
{ provide: 'LOGGER', useExisting: PinoLoggerService }The token (provide:) is how the container identifies a provider. For class providers the token is the class itself, which is why private users: UsersService works without any extra annotation. For string or symbol tokens, pull the value in with @Inject('DB_CONNECTION') private db: Pool.
Think of each shape as a recipe variant: useValue is a recipe with the result already baked in, useClass points to a different set of instructions, useFactory runs the recipe yourself, and useExisting is an alias to a recipe already in the container.
Scope: singleton, request, transient
Providers are singletons by default. One instance per application, shared across every controller that injects it. Scope.REQUEST creates a fresh instance per HTTP request. Scope.TRANSIENT creates a fresh instance every time something injects the provider.
The singleton default is what makes two controllers share the same UsersService object in memory - a user created by one controller is immediately visible to the other with no extra work, because they hold a reference to the exact same object. Change the scope and that guarantee disappears.
One scope bug that trips people regularly: inject a Scope.REQUEST provider into a default-scoped service and the singleton captures the first request's instance and holds it forever. On a Nest 10 project we injected a request-scoped TenantContext into a default-scoped BillingService, and every request started sharing the first tenant's context because the outer singleton never released it. The fix was to make the whole dependency chain request-scoped, not just the leaf provider. The Nest docs warn about this, but most people find that page after the bug, not before.
Common mistakes
Forgetting @Injectable()
// Wrong - no decorator
export class UsersService {
findAll() { return []; }
}
// Nest skips this class during provider resolution; controller receives undefinedAdd @Injectable() and the container recognizes the class.
Listing a provider in the wrong module
// Wrong - UsersService declared in AuthModule but UsersController is in UsersModule
@Module({ providers: [UsersService] })
export class AuthModule {}
@Module({ controllers: [UsersController] }) // no imports: [AuthModule]
export class UsersModule {}
// Error: Nest can't resolve dependencies of UsersControllerUsersModule must either declare UsersService in its own providers, or AuthModule must export it and UsersModule must import AuthModule. Forgetting the exports line is the most common reason this error fires on a fresh setup.
Circular dependency without forwardRef
// UsersService injects AuthService, AuthService injects UsersService
// Without forwardRef: bootstrap crashes with a circular dependency error
// With forwardRef:
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => AuthService)) private auth: AuthService
) {}
}Scope mismatch: request-scoped inside a singleton
// Wrong: TenantContext is REQUEST-scoped, BillingService is singleton
@Injectable()
export class BillingService {
constructor(private tenant: TenantContext) {} // captures first request's tenant forever
}
// Fix: make BillingService request-scoped too
@Injectable({ scope: Scope.REQUEST })
export class BillingService {
constructor(private tenant: TenantContext) {} // fresh instance per request
}Real-world usage
- NestJS CLI apps - every generated service (
AuthService,UsersService) is a provider by default - Prisma -
PrismaService extends PrismaClientregistered as a provider and injected into repositories - TypeORM - entity repos via
@InjectRepository(User), which is a custom provider under the hood - BullMQ - queue processors as providers injected into controllers or other services
- ConfigService - global config provider loaded once at bootstrap, injected anywhere via constructor
- Testing -
Test.createTestingModule({ providers: [...] }).overrideProvider(UsersService).useValue(mockService)swaps a real service for a mock without touching source code
Follow-up questions
Q: How does Nest know which class to inject for a constructor parameter?
A: TypeScript emits the parameter types into compiled output when emitDecoratorMetadata is true. Nest reads that metadata via reflect-metadata, matches each type to a token in the module's provider registry, and resolves the instance. Without the flag, every parameter shows up as Object and injection fails.
Q: What is the difference between singleton, request, and transient scope?
A: Singleton (default) creates one instance per application shared everywhere. Scope.REQUEST creates a new instance per HTTP request. Scope.TRANSIENT creates a new instance for every injection point. Use @Injectable({ scope: Scope.REQUEST }) to opt out of the singleton default.
Q: When would you use useFactory instead of useClass?
A: When the provider needs async initialization or arguments the constructor cannot receive from the container. Database connection pools, Redis clients, and config-driven factory functions are all useFactory cases. useClass is for synchronous class substitution only.
Q: How do you mock a provider in tests without jest.mock()?
A: Create a TestingModule and call .overrideProvider(UsersService).useValue(mockService) before .compile(). This swaps the provider in the container graph without touching global module state, which makes test isolation cleaner than module-level mocking.
Q: Can NestJS DI work without TypeScript?
A: Class-based injection relies on emitDecoratorMetadata, so plain JavaScript cannot use it directly. The workaround is custom providers with explicit inject arrays, which tell the container exactly what to pass in without relying on reflected types.
Q: Is a service the same thing as a provider?
A: A service is a provider, but a provider is not always a service. Repositories, factories, value bindings, and connection pools are all providers in the IoC container even though none of them fit the "service" label. The container does not care what you call the class.
Examples
Basic: controller injecting a service
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private list = ['Alice', 'Bob'];
findAll() { return this.list; }
findOne(name: string) {
return this.list.find(u => u === name) ?? null;
}
}
// users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get()
findAll() { return this.users.findAll(); }
@Get(':name')
findOne(@Param('name') name: string) { return this.users.findOne(name); }
}
// GET /users -> ["Alice", "Bob"]
// GET /users/Alice -> "Alice"
// GET /users/Charlie -> nullThe controller has no idea how UsersService is built. It declares the type in the constructor and the container handles the rest. No factory, no new, no manual wiring.
Intermediate: custom provider for repository injection
This pattern appears in e-commerce or SaaS backends where you want to swap the database layer for a mock in tests without changing AuthService.
// users.repository.ts
export interface UsersRepository {
findByEmail(email: string): Promise<{ id: number; email: string } | null>;
}
// prisma-users.repository.ts
@Injectable()
export class PrismaUsersRepository implements UsersRepository {
constructor(private prisma: PrismaService) {}
async findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
}
// auth.module.ts
@Module({
providers: [
AuthService,
{
provide: 'USER_REPO', // string token for the interface
useClass: PrismaUsersRepository, // swap to MockUsersRepository in tests
},
],
controllers: [AuthController],
})
export class AuthModule {}
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
@Inject('USER_REPO') private usersRepo: UsersRepository,
) {}
async validateUser(email: string) {
const user = await this.usersRepo.findByEmail(email);
return user ? { id: user.id, email: user.email } : null;
}
}
// validateUser('alice@example.com') -> { id: 1, email: 'alice@example.com' }
// validateUser('nobody@example.com') -> nullThe string token 'USER_REPO' decouples AuthService from the concrete class. In the test module, replace useClass: PrismaUsersRepository with useValue: { findByEmail: jest.fn() } and the service never knows the difference.
Senior: async factory provider for a database pool
// database.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Pool } from 'pg';
@Module({
providers: [
{
provide: 'DB_POOL',
useFactory: async (config: ConfigService): Promise<Pool> => {
const pool = new Pool({ connectionString: config.get('DATABASE_URL') });
await pool.connect(); // verify connection at bootstrap, not at first request
return pool;
},
inject: [ConfigService], // container resolves ConfigService first, then runs factory
},
],
exports: ['DB_POOL'],
})
export class DatabaseModule {}
// orders.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class OrdersService {
constructor(@Inject('DB_POOL') private db: Pool) {}
async findByUser(userId: number) {
const { rows } = await this.db.query(
'SELECT * FROM orders WHERE user_id = $1',
[userId]
);
return rows;
}
}The factory runs once during NestFactory.create(). If the database URL is wrong or the server is unreachable, the app crashes at startup instead of failing silently on the first request. That is exactly the point of async factory providers.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.