The centurion from the previous lesson takes orders and passes them on. But to whom? If he calculated the wages, checked the stores and wrote to the treasury himself, he would stop being a centurion and become a one-man cohort - and the same work would have to be repeated in every other unit.
A legion has specialists for that: a smith, a medic, a quartermaster. Each knows one thing and does it for everyone. In NestJS such a specialist is a service.
A service is an ordinary class with a single decorator:
1@Injectable()
2export class LegionService {
3 private legions: Legion[] = [];
4
5 findAll(): Legion[] {
6 return this.legions;
7 }
8
9 findById(id: number): Legion {
10 return this.legions.find((legion) => legion.id === id);
11 }
12
13 recruit(data: CreateLegionDto): Legion {
14 const legion = { id: Date.now(), ...data };
15 this.legions.push(legion);
16 return legion;
17 }
18
19 dismiss(id: number): void {
20 this.legions = this.legions.filter((legion) => legion.id !== id);
21 }
22}
marks a class as a provider that can be injected. That is all it does - and it is worth separating from its neighbours, because NestJS decorators look alike: a controller is marked @Injectable()
@Controller(), a module @Module(), and middleware is a class implementing NestMiddleware. @Injectable() turns a class into none of those - it says only that NestJS should know how to hand it to somebody.The four methods above are the typical set:
returns everything, findAll()
one item, findById(id)
creates a new one, recruit(data)
removes one. The names describe an action in the domain, not an HTTP verb - because a service does not know HTTP exists.dismiss(id)
The controller receives the ready specialist through its constructor:
1@Controller('legions')
2export class LegionController {
3 constructor(private readonly legionService: LegionService) {}
4
5 @Get()
6 findAll() {
7 return this.legionService.findAll();
8 }
9}The written order is fixed:
, then constructor(
, then private readonly legionService:
, and finally LegionService
.) {}
This shorthand deserves an explanation, because it looks odd. The word
private before a constructor parameter is a TypeScript shortcut: it declares a class field and assigns it a value in one place. Without it you would have to write private legionService: LegionService separately and this.legionService = legionService in the constructor body. readonly adds the guarantee that nobody will swap that field later.You never create the instance with
new - NestJS does. It looks at the parameter's type, finds a matching provider and hands it over ready. This is dependency injection: a class states what it needs, not where to get it.Specialists use one another's services in exactly the same way. The path has four steps, worth knowing in order:
@Injectable() to the new service.1@Injectable()
2export class PayrollService {
3 constructor(private readonly legionService: LegionService) {}
4
5 calculateTotalPay(): number {
6 const legions = this.legionService.findAll();
7
8 return legions.reduce((sum, legion) => sum + legion.pay, 0);
9 }
10}Step two is the one most often forgotten. A provider declared in a module is not automatically visible elsewhere - it must be listed in its module's
exports, and the module that needs it must import that module. Skipping this gives an unresolved-dependency error at startup.Our
LegionService keeps data in an array. When a real database arrives, every method will have to change - and with it everything that uses them.The answer is the Repository Pattern - the pattern that separates data access logic from business logic. The service then says what it wants done, while the repository knows how to fetch the data. Swapping an array for a database touches the repository alone.
Do not confuse it with other patterns whose names sound similar: Singleton ensures a single instance exists, Observer broadcasts notifications about events, Proxy puts a stand-in object in place of the real one. Only Repository is about separating data from logic.
Usually you name a class and NestJS creates it. Sometimes, though, the instance depends on something you learn only at startup - then you give a recipe instead of a class:
1@Module({
2 providers: [
3 {
4 provide: 'STORAGE_SERVICE',
5 useFactory: (config: ConfigService) => {
6 return config.get('STORAGE') === 'cloud'
7 ? new CloudStorageService()
8 : new LocalStorageService();
9 },
10 inject: [ConfigService],
11 },
12 ],
13})
14export class StorageModule {}Three fields describe that recipe.
is the name the provider will be available under. provide
is the function creating the instance - here it picks between two implementations depending on configuration. useFactory
lists what the factory itself needs; NestJS passes those in as arguments, in the same order.inject
Note what all this trouble buys: the rest of the application asks for
'STORAGE_SERVICE' and does not know which implementation it received. Swapping cloud for local disk is a change in one place, @name - not in every service that writes files.The specialists are in place, each knowing their own craft:
@Injectable() marks a class as a provider that can be injected - it does not make it a controller, a module or middleware,findAll, findById, recruit, dismiss), not HTTP verbs,constructor(, private readonly name: , ServiceType, ) {},private before a constructor parameter declares a field and assigns its value in one place,new - NestJS matches a provider by type,@Injectable() → import the module that exports it → inject through the constructor → call the method,exports,provide (the name), useFactory (the creating function) and inject (the factory's dependencies),In the next lesson you will build a first REST API out of these pieces - a full set of CRUD operations on one resource. For now remember: the controller knows who should do something, the service knows how - and only that lets you call the same knowledge from anywhere in the application.