We use cookies to enhance your experience on the site
CodeWorlds

Services and Providers - the Empire's specialists

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.

The decorator that makes a specialist

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}

@Injectable()
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
@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:

findAll()
returns everything,
findById(id)
one item,
recruit(data)
creates a new one,
dismiss(id)
removes one. The names describe an action in the domain, not an HTTP verb - because a service does not know HTTP exists.

Injecting into a controller

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:

constructor(
, then
private readonly legionService: 
, then
LegionService
, and finally
) {}
.

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.

A service inside a service

Specialists use one another's services in exactly the same way. The path has four steps, worth knowing in order:

  1. Add
    @Injectable()
    to the new service.
  2. Import the module that exports the service - if it comes from another module.
  3. Inject the service through the constructor.
  4. Call a method of the injected 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.

Separating data access

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.

A factory provider

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.

provide
is the name the provider will be available under.
useFactory
is the function creating the instance - here it picks between two implementations depending on configuration.
inject
lists what the factory itself needs; NestJS passes those in as arguments, in the same order.

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.

Summary

The specialists are in place, each knowing their own craft:

  • a service holds the logic, a controller only directs requests,
  • @Injectable()
    marks a class as a provider that can be injected
    - it does not make it a controller, a module or middleware,
  • method names describe an action in the domain (
    findAll
    ,
    findById
    ,
    recruit
    ,
    dismiss
    ), not HTTP verbs,
  • injection in order:
    constructor(
    ,
    private readonly name: 
    ,
    ServiceType
    ,
    ) {}
    ,
  • private
    before a constructor parameter declares a field and assigns its value in one place,
  • you never create the instance with
    new
    - NestJS matches a provider by type,
  • a service inside a service:
    @Injectable()
    → import the module that exports it → inject through the constructor → call the method
    ,
  • a provider is invisible outside its module until it reaches
    exports
    ,
  • the Repository Pattern separates data access logic from business logic - Singleton, Observer and Proxy solve entirely different problems,
  • a factory provider is described by
    provide
    (the name),
    useFactory
    (the creating function) and
    inject
    (the factory's dependencies),
  • thanks to a factory, the rest of the application does not know which implementation it got.

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.

Go to CodeWorlds