Video

Watch the full video on YouTube:

Modules in NestJS: imports, exports and @Global

Introduction

In the previous chapter (EP10: Dependency Injection and Providers) we introduced Services and demonstrated in PetsModule every dependency injection strategy NestJS offers: classes, pre-built values, sync or async factories, and useExisting to create aliases.

Now we are going to focus on something we have opened many times without ever talking about in detail: the module. In NestJS a module is, so to speak, a building block: the framework’s way of encapsulating specific functionality for a given feature, normally focused on a domain entity. In our case, the pets in PetsModule.

What Is a Module?

A module encapsulates the functionality of one feature: in PetsModule we have a controller for each pets endpoint, a dedicated service, and we also used it to demonstrate the dependency injection strategy models.

The golden rule is simple: there should be one module per feature. NestJS requires at minimum a single module (the AppModule), but for every feature we create we should have our own module.

Two Types of Modules

NestJS has two kinds of modules:

  1. Feature modules: encapsulate all the business logic related to a domain entity, such as PetsModule or CustomersModule.
  2. Shared modules: hold configuration, third-party library integrations, third-party APIs or reusable business logic across the whole API, such as a storage system.

And an important topic: by definition every module is a singleton. A single instance is created when the API boots up, and that instance lives for the whole process lifetime.

The Four Attributes of @Module

So far we had only worked with two attributes of the @Module decorator:

  • controllers: the entry points of our endpoints.
  • providers: the dependencies the module makes available to its services and controllers.

But the module has two more fundamental attributes:

  • exports: the dependencies of our module that we expose to other modules so they can reuse them.
  • imports: where we inject all those other modules we want to use in the current module.

Create a Full Module with nest g resource

Until now we had used nest g controller, nest g service and nest g module. The CLI also lets us generate a full module with resource, which creates DTOs, services, controllers, the module and even an entity:

pnpm nest g resource customers

Since the API is for a veterinary clinic, and we already have pets, now we are also going to create customers. The command generates the controller, the test file, the module, the service, the DTOs and the entity. The resulting customers.module.ts comes completely empty:

// customers.module.ts
import { Module } from "@nestjs/common"
import { CustomersController } from "./customers.controller"
import { CustomersService } from "./customers.service"

@Module({
  controllers: [CustomersController],
  providers: [CustomersService],
})
export class CustomersModule {}

The Classic Error: Dependencies Cannot Be Resolved

Suppose that inside CustomersModule we want to use PetService. In theory it would be enough to inject it in the constructor, but when we start the service we run into an error:

Nest can't resolve dependencies of the PetService (0). Please make sure that the argument ValueProvider at index 0 is available in the CustomersModule context.

And that is because PetService has its own dependencies: valueConfig (useValue), the factory provider and StorageManager with its two aliases (readable and writable).

The easy fix is to copy each of those providers into CustomersModule, but then the next error asks for the one at index one, and so on until StorageManager is copied too. The server boots, but it is not ideal.

Import the Module Instead of Copying Providers

The right approach is to import the whole module:

// customers.module.ts
import { Module } from "@nestjs/common"
import { PetsModule } from "../pets/pets.module"

@Module({
  imports: [PetsModule], // Import the module, not the providers one by one
})
export class CustomersModule {}

The Duplicated Instances Problem

When we copied PetService into the providers of CustomersModule, we can see in the console that the factory ran twice. That means we now have two instances of PetService and, for each instance, every dependency injection runs again.

That breaks the singleton nature of services. Services should be instantiated only once to save resources and improve API performance. By importing PetsModule we reuse the instance that already exists in the IOC container.

Exports: Exposing Services to Other Modules

With imports: [PetsModule] it should theoretically be enough, but if we try to inject PetService into CustomerService another error shows up: PetService cannot be found, even though it is part of PetsModule.

The solution is to export it from its module:

// pets.module.ts
import { Module } from "@nestjs/common"
import { PetsController } from "./pets.controller"
import { PetService } from "./pets.service"

@Module({
  controllers: [PetsController],
  providers: [PetService],
  exports: [PetService], // Tell other modules that PetService is available
})
export class PetsModule {}

This way PetsModule tells every module importing it that PetService is available, along with all its other dependencies (useValue, useFactory and useExisting). And notice it was injected only once: there is a single instance running.

Re-exporting Modules and the @Global Decorator

In some scenarios we can import modules that our modules are already using, for example a TypeORM database connection. If that import lives in PetsModule, any module using it gets that implementation too:

@Module({
  imports: [TypeOrmModule.forFeature([Pet])],
  exports: [TypeOrmModule], // Re-export the database module
})
export class PetsModule {}

We will see this in detail when we start integrating databases.

Global Modules with @Global

If we delete the imports attribute, the CustomerService constructor fails again because it cannot resolve PetService. An alternative is to make the module globally available with the @Global decorator from @nestjs/common:

import { Global, Module } from "@nestjs/common"

@Global()
@Module({
  controllers: [PetsController],
  providers: [PetService],
  exports: [PetService],
})
export class PetsModule {}

By doing this, NestJS adds the module to the global module registry and resolves all of its dependencies on its own through the inversion of control container, without us having to import it explicitly.

Best Practices

  1. One module per feature: every domain entity (pets, customers, orders) should have its own module.
  2. Prefer importing the whole module over copying providers one by one: you avoid duplicated instances and possible circular injections.
  3. Remember that modules and services are singletons: share resources whenever you can.
  4. Export only what other modules actually need with exports.
  5. Use @Global with real restraint: the more global modules we have, the harder it becomes to control them and profile which modules are being injected.
  6. Reserve global modules for authentication, authorization, access roles, database connections or storage: if your orders module does not communicate with the users module, neither of them needs to be global.

Conclusion

Modules are the building blocks of NestJS and the foundation of its modular architecture. Understanding that they are singletons, knowing the difference between feature and shared modules, and mastering imports, exports and @Global will let you build APIs that are more organized, reusable and resource efficient.

Repository

💻 Repository: https://github.com/radagva/guia-de-nestjs-example

ES6: 44 Features You Should KnowiOS Development Guide EP01: Install Xcode and tour its interface