NestJS course Β· Module 1: NestJS Basics
Modules and Dependency Injection
In this lesson6
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:
- Define a provider with
@Injectable()- a class ready to be injected. - Register it in the module's
providersarray - NestJS learns such a class exists. - Inject it through the constructor wherever it is needed.
- 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 in the other modules. You still register the global module itself once, usually in the imports of the root module. 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. 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
@Modulefields:controllers,providers,imports,exports- there is noroutesfield, routes follow from controller decorators, - a service in
exportsis available to importing modules; without it, it works internally only, exportsremoves nothing and changes nothing - it is purely a visibility declaration,- the four injection steps:
@Injectable()β register inprovidersβ 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, useValueregisters a ready value,providegives 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.
Code for this lesson: src/legion.module.ts
1// Modules and Dependency Injection - Legion Organization
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { LegionService } from './legion.service';
5import { ArmorService } from './armor.service';
6
7console.log("Organizing the legion with modules and DI!");
8
9// ===========================================
10// 1. Basic module - legion structure
11// ===========================================
12
13@Module({
14 controllers: [LegionController],
15 providers: [LegionService, ArmorService],
16 exports: [LegionService], // We expose for other modules
17})
18export class LegionModule {}
19
20// ===========================================
21// 2. Province module importing LegionModule
22// ===========================================
23
24import { ProvinciaController } from './provincia.controller';
25import { ProvinciaService } from './provincia.service';
26
27@Module({
28 imports: [LegionModule], // Import gives access to exports
29 controllers: [ProvinciaController],
30 providers: [ProvinciaService],
31})
32export class ProvinciaModule {}
33
34// ===========================================
35// 3. Main Imperium module
36// ===========================================
37
38@Module({
39 imports: [LegionModule, ProvinciaModule],
40})
41export class AppModule {}
42
43// ===========================================
44// 4. Dependency Injection in practice
45// ===========================================
46
47import { Injectable } from '@nestjs/common';
48
49@Injectable()
50export class ArmorService {
51 getArmorStrength(type: string): number {
52 const armorMap: Record<string, number> = {
53 lorica_segmentata: 90,
54 lorica_hamata: 70,
55 lorica_squamata: 60,
56 scutum: 50,
57 };
58 return armorMap[type] || 30;
59 }
60}
61
62@Injectable()
63export class LegionService {
64 // DI - NestJS automatically injects ArmorService
65 constructor(private readonly armorService: ArmorService) {
66 console.log("LegionService created with ArmorService!");
67 }
68
69 calculateDefense(armorType: string, soldiers: number): number {
70 const armorStrength = this.armorService.getArmorStrength(armorType);
71 return armorStrength * soldiers;
72 }
73}
74
75console.log("\n=== DEPENDENCY INJECTION ===");
76console.log("1. @Module declares controllers, providers, exports");
77console.log("2. imports allows using exports from another module");
78console.log("3. NestJS automatically injects dependencies via constructor");
79console.log("4. @Injectable() marks a class as a provider");
80Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which of the following fields is NOT part of the @Module() decorator in NestJS?
2. What does placing a service in the module's 'exports' array mean?
3. Which decorator makes a module globally available without the need to import it?
Hands-on tasks in the game
- Code editor
Define a LegionModule with the @Module() decorator that registers LegionController in controllers, LegionService in providers, and makes LegionService available to other modules through exports.
- Vertical ordering
Arrange the steps of the Dependency Injection process in NestJS from start to finish:
- Click in order
Arrange the elements of the Module decorator import in the correct order:
- Code editor
Define a configProvider constant with provide: 'CONFIG_TOKEN' and useValue holding a configuration object (e.g. { legion: 'Legio X Equestris' }) and add it to the providers of AppModule. Write a LegionConfigService that injects this value through the constructor with the @Inject('CONFIG_TOKEN') decorator and returns it from a getConfig() method.