Providers & Dependency Injection
Providers are where the real work of a NestJS application happens. Services, repositories, factories, and helpers are all providers, plain classes that the framework can instantiate and inject wherever they are needed. Dependency injection (DI) is the mechanism that wires them together, and it is the single most important concept to internalize when learning Nest.
What makes a class injectable
A provider is a class annotated with @Injectable(). The decorator marks it as a candidate for Nest’s inversion-of-control container, which manages its lifecycle and resolves its dependencies.
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
create(cat: Cat): Cat {
this.cats.push(cat);
return cat;
}
}
Constructor injection
To use a provider, declare it as a constructor parameter. Nest reads the parameter’s type, looks it up in the container, and supplies the instance, you never call new yourself.
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
}
The private readonly shorthand both declares and assigns the field. For DI to resolve, the provider must be listed in the module’s providers array, that registration is what binds the token to a class.
Nest resolves dependencies by type/token, not by parameter name. The container builds a dependency graph once at startup and caches singletons, so injection is essentially free at request time.
Custom providers
The shorthand providers: [CatsService] is sugar for { provide: CatsService, useClass: CatsService }. The full object form unlocks powerful patterns when you need to control what gets injected for a given token.
| Form | Purpose |
|---|---|
useClass | Bind a token to a class (swap implementations) |
useValue | Inject a constant, config object, or mock |
useFactory | Compute the provider at runtime, may be async and have its own deps |
useExisting | Create an alias for an existing provider |
import { Module } from '@nestjs/common';
const CONFIG = {
provide: 'APP_CONFIG',
useValue: { apiUrl: 'https://api.example.com', timeout: 5000 },
};
const dbFactory = {
provide: 'DB_CONNECTION',
useFactory: async (config: AppConfig) => connect(config.dbUrl),
inject: [ConfigService],
};
@Module({
providers: [CONFIG, dbFactory],
})
export class CoreModule {}
String-token providers are injected with the @Inject() decorator, since there is no class type to infer from:
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class ApiClient {
constructor(@Inject('APP_CONFIG') private config: AppConfig) {}
}
Provider scopes
By default every provider is a singleton, instantiated once and shared across the entire application. This is the most performant choice and the right default. When you need per-request state, change the scope.
| Scope | Lifetime |
|---|---|
Scope.DEFAULT | Singleton (shared across the app) |
Scope.REQUEST | A new instance per incoming request |
Scope.TRANSIENT | A new instance for every consumer that injects it |
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {}
Request-scoped providers carry a cost: they “bubble up”, any provider or controller that depends on them also becomes request-scoped, defeating singleton caching. Reach for request scope only when you genuinely need per-request isolation (e.g. multi-tenant context).
The repository pattern
Separating data access from business logic keeps services testable and persistence-agnostic. A repository encapsulates storage; the service orchestrates domain rules.
@Injectable()
export class CatsRepository {
private readonly store = new Map<string, Cat>();
findById(id: string): Cat | undefined {
return this.store.get(id);
}
save(cat: Cat): Cat {
this.store.set(cat.id, cat);
return cat;
}
}
@Injectable()
export class CatsService {
constructor(private readonly repo: CatsRepository) {}
adopt(id: string): Cat {
const cat = this.repo.findById(id);
if (!cat) throw new NotFoundException(`Cat ${id} not found`);
cat.adopted = true;
return this.repo.save(cat);
}
}
Because the service depends on CatsRepository by token, you can swap the in-memory implementation for a TypeORM or Prisma-backed one via useClass, without touching the service or its tests.
Best Practices
- Default to singleton scope. Only escalate to request/transient scope when state truly must be per-request.
- Depend on abstractions — inject by interface-style tokens so implementations can be swapped and mocked.
- Keep services focused. A provider that does five unrelated things is a code smell; split it.
- Use
useFactoryfor async setup (DB connections, secrets fetching) instead of doing work in a constructor. - Separate persistence from logic via the repository pattern to keep unit tests fast and storage-independent.