When building applications with NestJS, one of the key concepts to master is scopes. Scopes determine the lifecycle of provider instances, such as services, repositories, or custom providers. Choosing the right scope is essential for managing how instances are created, shared, and destroyed within your application. In this blog, weβll explore the three main types of scopes in NestJS, their use cases, and how to implement them effectively. Letβs dive in! πββοΈ
What Are Scopes in NestJS? π€
In NestJS, a scope defines how long a provider instance lives and how it is shared across your application. NestJS provides three types of scopes:
- DEFAULT (Singleton) π
- REQUEST π¨
- TRANSIENT π
Each scope serves a specific purpose and is suited for different scenarios. Letβs break them down one by one. π
1. DEFAULT (Singleton) Scope π
What Is It? π€·ββοΈ
The DEFAULT scope, also known as the singleton scope, is the most commonly used scope in NestJS. When a provider is singleton-scoped, a single instance of the provider is created when the application starts. This instance is then shared across the entire application.
Use Case π―
Singleton-scoped providers are ideal for stateless services or providers that donβt need to maintain request-specific data. Since the instance is reused, itβs highly performant and memory-efficient.
Example π»
@Injectable()
export class MyService {
constructor() {
console.log('MyService instance created');
}
doSomething() {
return 'Hello from MyService!';
}
}
- The
MyService
instance is created once when the application starts and reused everywhere itβs injected.
2. REQUEST Scope π¨
What Is It? π€·ββοΈ
The REQUEST scope creates a new instance of the provider for each incoming HTTP request. Once the request is completed, the instance is garbage-collected.
Use Case π―
Request-scoped providers are useful when you need to maintain request-specific data, such as user information or request context. This scope ensures that each request has its own isolated instance of the provider.
Example π»
@Injectable({ scope: Scope.REQUEST })
export class UserService {
private userId: string;
setUserId(id: string) {
this.userId = id;
}
getUserId() {
return this.userId;
}
}
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post(':id')
async setUser(@Param('id') id: string) {
this.userService.setUserId(id);
return this.userService.getUserId();
}
}
- In this example,
UserService
is request-scoped. Each HTTP request gets its own instance ofUserService
, ensuring that theuserId
is isolated to the specific request.
3. TRANSIENT Scope π
What Is It? π€·ββοΈ
The TRANSIENT scope creates a new instance of the provider each time it is injected into another provider or controller. Unlike the singleton scope, transient-scoped providers are not shared.
Use Case π―
Transient-scoped providers are useful when you need a fresh instance every time the provider is used. This is particularly helpful for services that maintain internal state or need to be isolated.
Example π»
@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService {
private logs: string[] = [];
log(message: string) {
this.logs.push(message);
console.log(message);
}
getLogs() {
return this.logs;
}
}
- Every time
LoggerService
is injected, a new instance is created. This ensures that thelogs
array is unique to each instance.
How to Set Scopes βοΈ
You can set the scope of a provider in two ways:
1. Using the @Injectable()
Decorator π¨
@Injectable({ scope: Scope.REQUEST })
export class MyService {}
2. Using the provide
Property in a Custom Provider π οΈ
{
provide: 'MY_SERVICE',
useClass: MyService,
scope: Scope.REQUEST,
}
Key Considerations π§
Performance β‘
- Singleton: Most performant, as instances are reused. π
- Request: Can increase memory usage due to creating instances per request. π§©
- Transient: Can increase overhead due to creating instances per injection. π
State Management ποΈ
- Use request-scoped providers for request-specific data. π¨
- Use transient-scoped providers for isolated state. π
Dependency Injection π
- Singleton-scoped providers cannot directly depend on request-scoped providers. Use the
@Inject()
decorator withREQUEST
orCONTEXT
to handle such cases. π οΈ
Real-World Example: Request-Scoped Authentication Service π
Letβs say youβre building an authentication system where you need to store user information for the duration of a request. A request-scoped service is perfect for this scenario.
@Injectable({ scope: Scope.REQUEST })
export class AuthService {
private user: User;
setUser(user: User) {
this.user = user;
}
getUser() {
return this.user;
}
}
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() credentials: { username: string; password: string }) {
const user = await validateUser(credentials); // Validate user
this.authService.setUser(user);
return { message: 'Logged in successfully', user: this.authService.getUser() };
}
}
- The
AuthService
is request-scoped, ensuring that theuser
data is isolated to the specific request.
Conclusion π
Understanding and using scopes effectively in NestJS is crucial for building scalable and maintainable applications. Hereβs a quick summary of when to use each scope:
- Singleton (DEFAULT): Use for stateless services or providers that donβt need request-specific data. π
- REQUEST: Use for providers that need to maintain request-specific data. π¨
- TRANSIENT: Use for providers that need a fresh instance every time they are injected. π
By choosing the right scope, you can optimize performance, manage state effectively, and ensure your application runs smoothly. π
If you found this guide helpful, feel free to share it with your fellow developers. For more NestJS tips and tutorials, stay tuned! Happy coding! π»β¨
Top comments (0)