We use cookies to enhance your experience on the site
CodeWorlds

Modules and Dependency Injection

You already have a controller and a service. But how does NestJS know that controller exists? And where does it get the service it injects into its constructor? Classes sitting in files mean nothing on their own - somebody has to declare them.

The Empire did not govern its provinces from a single desk. Each had its own administration: its register of offices, its specialists, and a list of what it made available to its neighbours. The emperor knew only the list of provinces, not every clerk. In NestJS such a province is a module.

The @Module decorator

A module is a class with a decorator listing its contents:

1import { Module } from '@nestjs/common';
2
3@Module({
4  imports: [DatabaseModule],
5  controllers: [LegionController],
6  providers: [LegionService],
7  exports: [LegionService],
8})
9export class LegionModule {}

The decorator's import has a fixed shape -

import
,
{ Module }
,
from
,
'@nestjs/common'
.

Four fields describe the whole province, and each answers a different question:

controllers
- who receives requests.
providers
- which specialists work here; this is where you register services.
imports
- which other modules we use.
exports
- what we make available outside.

Those are all the fields of the

@Module
decorator. There is no
routes
field among them - routes are not configured in a module; they follow from the controller decorators you met earlier.

exports - what is visible across the border

The

exports
field is the one that causes the most trouble, so let's name it outright: a service listed in
exports
will be available to the modules that import this module
.

Which implies the reverse - a service registered in

providers
but absent from
exports
works only inside its own module. Another module trying to inject it will get an unresolved-dependency error at startup. That is precisely the error that shows up in half of all first NestJS projects.

Note what

exports
does not do: it does not remove the service from the module, does not turn it into a controller, and has nothing to do with saving to a database. It is purely a declaration of visibility.

The default of being closed is deliberate. A module is meant to say explicitly what it shares - which keeps the boundaries visible and stops accidental dependencies forming between distant parts of the application.

The four steps of injection

Since a module registers providers, we can now assemble the whole dependency path. It has four steps, always in this order:

  1. Define a provider with
    @Injectable()
    - a class ready to be injected.
  2. Register it in the module's
    providers
    array
    - NestJS learns such a class exists.
  3. Inject it through the constructor wherever it is needed.
  4. Use the service in the class's methods.

Step two is the one easily forgotten when adding a new service: the

@Injectable()
decorator alone is not enough. NestJS builds its dependency map from what the modules list, not from scanning files.

A global module

Some things are useful everywhere - configuration, a logger, a database connection. Importing their module into every other module is then just repetition:

1@Global()
2@Module({
3  providers: [ConfigService],
4  exports: [ConfigService],
5})
6export class ConfigModule {}

The

@Global()
decorator makes a module available without importing it. Mind the name - there are no
@Shared()
,
@Public()
or
@Universal()
decorators.

One thing does not change despite the global status:

exports
still applies.
@Global()
frees you from importing the module, but it is
exports
that decides what the module shares at all.

Use it sparingly, @name. A global module disappears from import lists, so it stops being visible who depends on what - and that is the information that saves you on a larger project. Configuration and a logger, yes; anything else, probably not.

A provider under a name

So far we recognised providers by their class type. But you can also inject a plain value - an object, an array, a number - and those have no type:

1@Module({
2  providers: [
3    {
4      provide: 'CONFIG_TOKEN',
5      useValue: { apiUrl: 'https://api.imperium.rome', timeout: 5000 },
6    },
7  ],
8})
9export class AppModule {}

useValue
supplies a ready value instead of a class to instantiate, and
provide
gives it a name - a token it will be recognised by.

Since there is no type here for NestJS to match a provider by, we name the token explicitly when injecting:

1@Injectable()
2export class ApiService {
3  constructor(@Inject('CONFIG_TOKEN') private config: { apiUrl: string }) {}
4}

@Inject('CONFIG_TOKEN')
says: give me what was registered under that name. With ordinary services the decorator is unnecessary, because the type suffices - here it is required.

Summary

The provinces have their administrations, the emperor knows only their list:

  • a module groups related elements and declares them to NestJS - classes in files mean nothing on their own,
  • the decorator's import:
    import
    ,
    { Module }
    ,
    from
    ,
    '@nestjs/common'
    ,
  • the four
    @Module
    fields:
    controllers
    ,
    providers
    ,
    imports
    ,
    exports
    - there is no
    routes
    field
    , routes follow from controller decorators,
  • a service in
    exports
    is available to importing modules
    ; without it, it works internally only,
  • exports
    removes nothing and changes nothing - it is purely a visibility declaration,
  • the four injection steps:
    @Injectable()
    → register in
    providers
    → inject through the constructor → use in methods
    ,
  • @Injectable()
    alone is not enough - NestJS builds its dependency map from modules, not from scanning files,
  • @Global()
    makes a module available without importing
    ;
    @Shared()
    ,
    @Public()
    and
    @Universal()
    do not exist,
  • being global does not exempt you from
    exports
    - and use it sparingly, because it hides dependencies,
  • useValue
    registers a ready value,
    provide
    gives it a token, and
    @Inject('TOKEN')
    names that token when injecting.

In the next lesson you will meet controllers from the inside - you will see how a request reaches the right method. For now remember: a module is a province's administration - it registers who works here, and says explicitly what it lends to its neighbours.

Go to CodeWorlds