Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

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.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 37m
Published
Clock icon Feb 13, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. 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.

  2. 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.

  3. 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 the constructor() how the createdAt date is initialized and the console.log() message showing that the service has been created. This created 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 the src/default.controller.ts file.

    Building off of this knowledge, now apply the REQUEST scope to the RequestService

    Notice 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 the src/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.

  4. 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 injecting BService.

    @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:

    1. The order of instantiation is indeterminate. When using forwardRef or moduleRef you cannot rely on the order a class is instantiated. Make sure your code does not depend on which constructor is called first.
    2. 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 and ModuleRef. With these skills, you're ready to build scalable, maintainable NestJS applications. Great job!

Jeff Hopper is a polyglot solution developer with over 20 years of experience across several business domains. He has enjoyed many of those years focusing on the .Net stack.

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.