Introduction
By default, NestJS providers are singletons—one instance shared across the entire application. But sometimes you need request-specific instances or fresh instances on every injection. Injection scopes control provider lifecycle.
Key Concepts
- DEFAULT (Singleton): One instance for entire app lifetime
- REQUEST: New instance per HTTP request
- TRANSIENT: New instance on every injection
Real World Context
Use cases for different scopes:
- Singleton: Stateless services, database connections, caches
- Request: Request-specific data (user context, tenant info)
- Transient: Stateful helpers that shouldn't share state
Deep Dive
Singleton (Default)
typescript@Injectable() export class CatsService { // One instance shared across all requests }
Request Scope
typescriptimport { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class RequestContextService { private userId: string; setUserId(id: string) { this.userId = id; } getUserId(): string { return this.userId; } }
Transient Scope
typescript@Injectable({ scope: Scope.TRANSIENT }) export class HelperService { private state: any; // Fresh instance every time it's injected }
Scope Bubbling
When a singleton depends on a request-scoped provider, the singleton becomes request-scoped too:
typescript@Injectable() // Would become request-scoped export class ServiceA { constructor(private requestScoped: RequestContextService) {} }
Accessing Request in Request-Scoped Providers
typescriptimport { REQUEST } from '@nestjs/core'; import { Request } from 'express'; @Injectable({ scope: Scope.REQUEST }) export class RequestService { constructor(@Inject(REQUEST) private request: Request) { console.log(request.url); } }
Common Pitfalls
- Scope bubbling surprises: Injecting request-scoped into singleton makes it request-scoped. Performance impact.
- Memory leaks with REQUEST scope: Many request-scoped providers = many instances. Monitor memory.
- Not understanding singleton default: Storing request data in singletons causes race conditions.
Best Practices
- Stick with singleton (default) unless you have a specific need
- Use request scope sparingly—it has performance overhead
- For user context, consider a request-scoped context service
- Document non-default scopes for team awareness
Summary
Provider scopes control instance lifecycle. DEFAULT (singleton) shares one instance, REQUEST creates per-request instances, and TRANSIENT creates fresh instances on every injection. Use the appropriate scope based on your statefulness requirements.
Code Examples
typescript
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
constructor(@Inject(REQUEST) private request: Request) {}
getUserId(): string {
return this.request.user?.id;
}
}