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.
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:
- who receives requests. controllers
- which specialists work here; this is where you register services. providers
- which other modules we use. imports
- what we make available outside.exports
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.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.
Since a module registers providers, we can now assemble the whole dependency path. It has four steps, always in this order:
@Injectable() - a class ready to be injected.providers array - NestJS learns such a class exists.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.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
decorator makes a module available without importing it. Mind the name - there are no @Global()
@Shared(), @Public() or @Universal() decorators.One thing does not change despite the global status:
still applies. exports
@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.
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 {}
supplies a ready value instead of a class to instantiate, and useValue
gives it a name - a token it will be recognised by.provide
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}
says: give me what was registered under that name. With ordinary services the decorator is unnecessary, because the type suffices - here it is required.@Inject('CONFIG_TOKEN')
The provinces have their administrations, the emperor knows only their list:
import, { Module }, from, '@nestjs/common',@Module fields: controllers, providers, imports, exports - there is no routes field, routes follow from controller decorators,exports is available to importing modules; without it, it works internally only,exports removes nothing and changes nothing - it is purely a visibility declaration,@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,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.