DEV Community

Cover image for Understanding Scopes in NestJS: A Comprehensive Guide πŸš€
Abhinav
Abhinav

Posted on

Understanding Scopes in NestJS: A Comprehensive Guide πŸš€

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:

  1. DEFAULT (Singleton) 🏠
  2. REQUEST πŸ“¨
  3. 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!';
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
  }
}
Enter fullscreen mode Exit fullscreen mode
  • In this example, UserService is request-scoped. Each HTTP request gets its own instance of UserService, ensuring that the userId 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;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Every time LoggerService is injected, a new instance is created. This ensures that the logs 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 {}
Enter fullscreen mode Exit fullscreen mode

2. Using the provide Property in a Custom Provider πŸ› οΈ

{
  provide: 'MY_SERVICE',
  useClass: MyService,
  scope: Scope.REQUEST,
}
Enter fullscreen mode Exit fullscreen mode

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 with REQUEST or CONTEXT 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() };
  }
}
Enter fullscreen mode Exit fullscreen mode
  • The AuthService is request-scoped, ensuring that the user 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)