- Lab
- Core Tech

Guided: Dependency Injection and Providers in NestJS
In this lab, you will learn how to implement dependency injection (DI) patterns in NestJS to build scalable and maintainable applications. You will start by creating and configuring providers, which are essential building blocks for DI. Then, you will explore how to manage different injection scopes to control the lifecycle of dependencies. Finally, you will tackle circular dependency resolution, ensuring that your services remain decoupled and functional even in complex scenarios. By mastering these concepts, you will enhance your ability to design efficient and modular NestJS applications.

Path Info
Table of Contents
-
Challenge
Introduction
Understanding Dependency Injection in NestJS
Introduction
Dependency Injection (DI) is a crucial design pattern in software engineering that promotes Inversion of Control (IoC) by ensuring that classes receive their dependencies from external sources rather than creating them internally. This approach enhances modularity, testability, and flexibility.
For instance, instead of a Car class instantiating its own Engine, an Engine instance is injected into the Car, making maintenance and testing significantly easier.
NestJS Providers and Dependency Injection
In NestJS, DI is implemented using providers, which encapsulate functionalities such as services, repositories, and factories. By leveraging NestJS modules and the
@Injectable()
decorator, these providers become available for injection across the application, promoting reusability and maintainable architecture.The NestJS IoC Container
NestJS leverages a built-in IoC container to efficiently manage providers. This container automatically handles the creation, configuration, and resolution of dependencies, reducing boilerplate code and leveraging TypeScript metadata for seamless integration.
The IoC container supports different lifecycle scopes, including:
- Singleton Scope: A single instance shared across the entire application.
- Request Scope: A new instance created per request.
- Transient Scope: A new instance created each time it is injected.
By using these scopes appropriately, developers can optimize resource management and application performance.
Next Steps
In the following steps you will:
- Learn how to create, register, and inject your own providers.
- Explore different provider lifecycle scopes.
- Investigate strategies for resolving circular dependencies.
If you need assistance at any point, you can refer to the
/solution
directory, which contains subdirectories for each of the following steps. -
Challenge
Create and Configure a Provider
Declaring and Configuring Providers
In this step, you will create your first provider (also known as a service) in a NestJS application.
Review the AppController
Open the
src/app.controller.ts
file. Inside this file, you will find a@Get('/welcome')
endpoint that currently returns a hard-coded string response:@Get('/welcome') getWelcomeMessage(): string { return "Hello, World"; }
Your task is to replace this hard-coded string with an injected service that will retrieve the welcome message.
Note that using the
start:dev
command will run the application in watch mode. So any changes made in the following steps will automatically compile your changes.You have successfully created and configured your first provider/service in NestJS. Providers help you structure applications in a modular and maintainable way. Next, think about how you can extend the
WelcomeService
to accept dynamic input or interact with a database. -
Challenge
Manage Different Injection Scopes
Manage Injection Scopes
Providers can have one of three scopes (or lifecycles): DEFAULT, REQUEST, or TRANSIENT.
These can be applied to a provider using the
@Injectable()
decorator using the format:@Injectable({ scope: Scope.DEFAULT})
While this decorator is unnecessary for simple services, it is required when a provider has dependencies or needs to declare a custom scope.
The following tasks explore each of these scopes using the ScopesModule. A controller and service has been provided for each scope to help demonstrate their behavior.
Verify that the application is still running in
start:dev
mode. As you make the changes below, the terminal output will update, providing a reference to demonstrate the lifecycle of each scope option. At this point, you should see that each service and controller have been created. > Notice in theconstructor()
how thecreatedAt
date is initialized and theconsole.log()
message showing that the service has beencreated
. Thiscreated
log message is what you are referencing in the terminal window. This will be important in the following tasks.Each of these services are already injected into their associated controller file, but if you would like to review that configuration you can look at the
DefaultController
in thesrc/default.controller.ts
file.Building off of this knowledge, now apply the
REQUEST
scope to the RequestServiceNotice that not only is a new service instance created, but a new controller as well. Declaring a provider with the
REQUEST
scope propagates up the injection chain, making all dependent classes request-scoped. Overusing this scope can significantly impact performance and memory usage.How to inject multiple instances?
If you're wondering how the TransientController injects multiple instances of the TransientService, open thesrc/transient.controller.ts
file.In this file, look for the use of the
moduleRef
class. This class traverses the internal registry of providers to retrieve a reference to any registered providers.Because the TransientService is declared as transient-scoped, the IoC container creates a new instance of the provider each time it is requested.
By completing these tasks, you have experienced how different scopes impact the lifecycle of providers and considerations on performance and functionality.
In the nest step you'll explore how to manage circular dependencies.
-
Challenge
Circular Dependency Resolution
Circular Dependency Resolution
Sometimes, you’ll run into a situation where two services depend on each other, creating a circular dependency. NestJS can’t resolve this automatically and will throw an error because it doesn’t know which service to instantiate first. Ideally, you refactor your code to avoid this, but if that’s not possible, there are ways to work around it.
Option 1: Forward Referencing
forwardRef
is a utility in NestJS that lets you reference a provider before it’s defined. It delays resolving the dependency until all providers are registered.To demonstrate
forwardRef
, you'll work with the AService and BService services in the CircularModule.Opening the
src/circular/a.service.ts
, you'll see that it is already successfully injectingBService
.@Injectable() Decorator
The
@Injectable()
decorator is essential for defining a provider's scope and ensuring NestJS recognizes its dependencies. In this case, the decorator is required because this service relies on another provider.
Your first task is to inject AService into BService working through the circular dependency this creates.
Option 2: Using ModuleRef
Another way to handle circular dependencies is using
ModuleRef
. This works by manually fetching the dependency at runtime instead of injecting it directly.You’ll apply this approach to help manage the circular dependency between CService and DService. > WARNINGS:
- The order of instantiation is indeterminate. When using
forwardRef
ormoduleRef
you cannot rely on the order a class is instantiated. Make sure your code does not depend on which constructor is called first. - Circular dependencies depending on providers with
Scope.REQUEST
can lead to undefined dependencies.
Congratulations
Congratulations on completing the lab! You covered essential NestJS concepts like dependency injection, provider configuration, injection scopes, and circular dependency resolution using
forwardRef
andModuleRef
. With these skills, you're ready to build scalable, maintainable NestJS applications. Great job! - The order of instantiation is indeterminate. When using
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.