We use cookies to enhance your experience on the site
CodeWorlds

NestJS architecture - building the Empire

A first project usually fits in one file. A second has five. By the twentieth nobody knows where the wage calculation lives - in a controller, in some helper, or perhaps in three places at once, each slightly different.

Rome did not grow by adding men to a single cohort. It grew because everyone had one role and everyone knew which: the messenger carries orders, the quartermaster counts stores, the scribe keeps the register. NestJS imposes the same division - and it is that imposed structure which keeps the twentieth file findable.

The pattern it stands on

The foundation of NestJS architecture is Model-View-Controller (MVC) - the pattern splitting an application into three responsibilities: the data, its presentation, and handling requests.

Do not confuse it with patterns solving entirely different problems: Singleton ensures a single instance of a class exists, Observer broadcasts notifications about events, Strategy lets you swap an algorithm at runtime. You will meet all three inside NestJS, but it is MVC that shapes the whole application.

On a backend the View layer usually disappears - the response is JSON, not an HTML page - so day to day you work with the other two and with the module that ties them together.

Three roles

A whole NestJS application is built from three recurring elements. They are worth remembering by the question each answers:

  • The controller - who receives the request? It handles HTTP requests and returns responses. It does not store data, does not deal with appearance, does not compile anything.
  • The service - how is it done? This is where the logic lives.
  • The module - what belongs together? It groups both and declares them to NestJS.

The service - where logic lives

Let's start from the middle, because the service carries the real value:

1@Injectable()
2export class LegionService {
3  private legions = [
4    { id: 1, name: 'Legio X Equestris' },
5    { id: 2, name: 'Legio III Gallica' },
6  ];
7
8  findAll() {
9    return this.legions;
10  }
11}

The declaration order is fixed:

@Injectable()
, then
export class
, then the class name, and finally the body with its methods.

And here comes the answer to the question that in practice decides a project's quality: business logic belongs in services - what NestJS calls providers. Not in controllers, not in middleware, not in configuration files.

The reason is simple. Logic in a controller is tied to HTTP - you cannot call it from a batch job or a unit test without faking a request. In middleware it is worse still, because middleware works on the raw request, before it is known where it is going. Logic in a service is an ordinary method on an ordinary class - you can call it from anywhere.

The controller - who receives the request

The controller takes the service through its constructor and delegates the work:

1@Controller('legions')
2export class LegionController {
3  constructor(private readonly legionService: LegionService) {}
4
5  @Get()
6  findAll() {
7    return this.legionService.findAll();
8  }
9}

Note the proportions. The controller method is one line - and that is how it should be. Its entire role is to receive the request and name the executor.

The controller does not create the service with

new
. It writes it into the constructor and NestJS hands it over ready - that is dependency injection, a mechanism you will meet in detail in the coming lessons. For now it is enough to see the effect: the controller states what it needs and does not worry where to get it.

The module - what belongs together

The third element ties the previous two:

1@Module({
2  controllers: [LegionController],
3  providers: [LegionService],
4})
5export class LegionModule {}

Without that declaration NestJS does not know these classes exist - files mean nothing on their own.

Note that a module shows the whole province at once: who receives requests and who does the work. Opening an unfamiliar project, you read the modules first, because they show the map.

A request's road

Let's assemble it into one run. A GET /legions request travels like this: it reaches the controller

LegionController
, which calls a method of the service
LegionService
, the service returns data, the controller hands it back as the response. The module takes no part in flight - it acted earlier, at startup, when NestJS built its dependency map from it.

This scheme repeats across the whole application, whatever its size. The twentieth resource looks exactly like the first - and that is why you know where to look for the wage calculation, @name: in a service, always in a service.

Summary

The Empire has its administration and everyone knows their role:

  • the foundation of NestJS architecture is Model-View-Controller (MVC) - not Singleton, not Observer, not Strategy,
  • on a backend the View layer usually disappears, because the response is JSON,
  • a controller handles HTTP requests and returns responses - it does not store data or deal with appearance,
  • business logic belongs in services (providers) - not in controllers, middleware or configuration files,
  • the reason: logic in a service is an ordinary method, so you can call it outside HTTP too,
  • service declaration in order:
    @Injectable()
    ,
    export class
    , the name, the body with methods,
  • a controller receives the service through its constructor, it does not create it with
    new
    ,
  • a controller method is usually one line - receive and delegate,
  • a module declares controllers and providers; without it NestJS does not see them,
  • a request's road: controller β†’ service β†’ data β†’ response; the module acted earlier, at startup.

In the next lesson we will look at modules more closely - you will see how they split an application into provinces and what decides whether one sees another. For now remember: the controller receives, the service performs, the module registers - and the same arrangement repeats in every corner of the application.

Go to CodeWorlds→