NestJS course Β· Module 1: NestJS Basics
NestJS architecture - building the Empire
In this lesson7
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.
What it stands on
The foundation of NestJS architecture is modules, controllers and providers connected by dependency injection (Dependency Injection). NestJS borrowed this layout from Angular - the framework's documentation says outright that its architecture is heavily inspired by Angular.
You may have heard of the Model-View-Controller (MVC) pattern. NestJS can work that way when the server renders HTML pages from templates, but it is an optional technique, not the foundation. A typical backend has no View layer at all - the response is JSON, not an HTML page.
Inside NestJS you will also meet other well-known patterns, e.g. every provider is a singleton by default - NestJS creates one instance of it for the whole application. That is a detail, though: the shape of the whole application comes from modules and dependency injection.
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: in a service, always in a service.
Summary
The Empire has its administration and everyone knows their role:
- the foundation of NestJS architecture is modules, controllers and providers connected by dependency injection - a layout borrowed from Angular,
- MVC is an optional technique for rendering pages from templates; a backend that returns JSON has no View layer,
- 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.
Code for this lesson: src/imperium.module.ts
1// Imperium Module - organization of legions and taxes
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { TributumController } from './tributum.controller';
5import { ProvinciaController } from './provincia.controller';
6import { LegionService } from './legion.service';
7import { TributumService } from './tributum.service';
8import { ProvinciaService } from './provincia.service';
9import { AquaeductService } from './aquaeduct.service';
10
11console.log("Loading Imperium Romanum Architecture...");
12
13// ===========================================
14// 1. Legion Module
15// ===========================================
16@Module({
17 controllers: [LegionController],
18 providers: [LegionService],
19 exports: [LegionService],
20})
21export class LegionModule {
22 constructor() {
23 console.log("Legion Module initialized - Ready to command armies!");
24 }
25}
26
27// ===========================================
28// 2. Tax Module - Tributum Module
29// ===========================================
30@Module({
31 imports: [LegionModule],
32 controllers: [TributumController],
33 providers: [TributumService],
34 exports: [TributumService],
35})
36export class TributumModule {
37 constructor() {
38 console.log("Tributum Module initialized - Ready to collect taxes!");
39 }
40}
41
42// ===========================================
43// 3. Province Module - Provincia Module
44// ===========================================
45@Module({
46 imports: [LegionModule, TributumModule],
47 controllers: [ProvinciaController],
48 providers: [ProvinciaService, AquaeductService],
49 exports: [ProvinciaService, AquaeductService],
50})
51export class ProvinciaModule {
52 constructor() {
53 console.log("Provincia Module initialized - All regions ready!");
54 }
55}
56
57// ===========================================
58// 4. Main application module - App Module
59// ===========================================
60@Module({
61 imports: [
62 LegionModule,
63 TributumModule,
64 ProvinciaModule,
65 ],
66 controllers: [],
67 providers: [
68 {
69 provide: 'IMPERIUM_CONFIG',
70 useValue: {
71 imperiumName: 'Roma Aeterna NestJS',
72 caesar: 'Augustus.js',
73 version: '1.0.0',
74 port: 3000,
75 }
76 }
77 ],
78})
79export class ImperiumAppModule {
80 constructor() {
81 console.log("Imperium App Module loaded successfully!");
82 console.log("All systems operational - Ready for conquest!");
83 }
84}
85
86// ===========================================
87// 5. Dependency Injection demonstration
88// ===========================================
89export class DependencyInjectionDemo {
90 static demonstrateModuleArchitecture() {
91 console.log("\n=== MODULE ARCHITECTURE ===");
92 console.log("LegionModule: Manages legions");
93 console.log(" βββ LegionController (REST endpoints)");
94 console.log(" βββ LegionService (business logic)");
95 console.log(" βββ exports: [LegionService]");
96
97 console.log("\nTributumModule: Manages taxes");
98 console.log(" βββ imports: [LegionModule]");
99 console.log(" βββ TributumController (REST endpoints)");
100 console.log(" βββ TributumService (business logic)");
101 console.log(" βββ exports: [TributumService]");
102
103 console.log("\nProvinciaModule: Manages provinces");
104 console.log(" βββ imports: [LegionModule, TributumModule]");
105 console.log(" βββ ProvinciaController (REST endpoints)");
106 console.log(" βββ ProvinciaService (business logic)");
107 console.log(" βββ AquaeductService (infrastructure logic)");
108 console.log(" βββ exports: [ProvinciaService, AquaeductService]");
109
110 console.log("\nImperiumAppModule: Main module");
111 console.log(" βββ imports: [LegionModule, TributumModule, ProvinciaModule]");
112 console.log(" βββ providers: [IMPERIUM_CONFIG]");
113 console.log(" βββ Global configurations");
114 }
115
116 static demonstrateDependencyFlow() {
117 console.log("\n=== DEPENDENCY FLOW ===");
118 console.log("1. Request β ProvinciaController");
119 console.log("2. ProvinciaController β ProvinciaService (DI)");
120 console.log("3. ProvinciaService β LegionService (DI via import)");
121 console.log("4. ProvinciaService β TributumService (DI via import)");
122 console.log("5. ProvinciaService β AquaeductService (DI)");
123 console.log("6. Response β ProvinciaController");
124
125 console.log("\nAdvantages of Dependency Injection:");
126 console.log(" Loose coupling");
127 console.log(" Easy testing");
128 console.log(" Code reusability");
129 console.log(" Maintainability");
130 }
131}
132
133DependencyInjectionDemo.demonstrateModuleArchitecture();
134DependencyInjectionDemo.demonstrateDependencyFlow();
135
136export { LegionModule, TributumModule, ProvinciaModule };Spotted 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. What are the building blocks of a NestJS application's architecture?
2. What is the main role of the Controller in NestJS architecture?
3. Where should business logic be located according to NestJS architecture?
Hands-on tasks in the game
- Code editor
Write a LegionService with the @Injectable() decorator and a findAll() method that returns an array of legionaries (at least one), e.g. [{ id: 1, name: 'Marcus Aurelius' }].
- Vertical ordering
Arrange the elements of a NestJS service declaration in the correct order:
- Code editor
Write a LegionController with the @Controller('legions') decorator that receives LegionService through the constructor and, in a findAll() method with the @Get() decorator, returns the result of this.legionService.findAll().