How NestJS Boots: Factory Pattern, Module Assembly, and Dependency Injection from Scratch
NestJS Getting Started: From the NestFactory Entry Point to the Module/Controller/Service Modular Structure Explained in One Go
You previously used Next.js for full-stack development (frontend and backend integrated). If you only want to do pure backend—providing APIs, system integration, microservices—there is a more "enterprise-grade" choice in the Node ecosystem: NestJS. It defaults to TypeScript, is fully modular, and is suitable for building large-scale services. Today we will start with two questions: How does a NestJS application "start up"? How does it manage to organize a massive backend so clearly? Following the scaffolded code and a Mixue Bingcheng demo, we will connect three threads: the entry point (factory pattern), the modular structure (Module/Controller/Service), and decorator-based assembly. A basic understanding of TypeScript classes and decorators is needed.
1. Why NestJS: The Enterprise-Grade Pure Backend Framework for Node
What exactly does backend development do? The instructor listed three areas: providing web API interfaces, system integration (concurrency, low-level services, AI Infra), and microservices. Next.js is "full-stack" (frontend and backend together), while NestJS focuses on the "pure backend" side—it is an enterprise-grade framework on Node, defaulting to TypeScript, with a core philosophy of comprehensive modularity. In other words, when a project is no longer a single page but dozens of interfaces and multiple business domains, NestJS uses modules to cut complexity into smaller pieces, rather than piling all logic into a single file thousands of lines long.
2. Scaffolding and Entry Point: NestFactory in main.ts
Installation and project setup (instructor's notes):
npm i -g @nestjs/cli # Install CLI globally
nest new hello # Generate scaffold
nest run start # Start (as written in the notes, equivalent to npm run start)
After generation, the two files in src/ you should look at first are: main.ts as the entry point, and app.module.ts as the root module. The entry point code is very short, but three lines reveal NestJS's startup mechanism:
// nestjs on-demand loading, performance optimization for large frameworks, modular thinking
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule); // Factory pattern, instantiate backend application
await app.listen(process.env.PORT ?? 3000); // Start HTTP service on port 3000
}
bootstrap();
When I first read this, I didn't immediately realize: isn't NestFactory.create(AppModule) literally a "factory"? You don't need to manually new up a whole set of application details; instead, you hand it off to the framework's factory method to create. Let's plant a flag here—we will revisit the factory pattern separately in section five using the Mixue Bingcheng demo, then come back to confirm it.
After NestFactory.create(AppModule), listen(3000) starts the HTTP service. A comment in main.ts also notes: the backend route localhost:3000/ will be "sent to AppModule". That is, AppModule is the overall assembly point for the entire application. The structure looks like this:
flowchart TD
MAIN[main.ts Entry] --> NF[NestFactory.create]
NF --> AM[AppModule Root Module]
subgraph MOD[AppModule Assembly]
AM --> CT[AppController Controller]
AM --> SV[AppService Service]
end
CT --> SV
Figure 1 explains one thing: the application is created by NestFactory, and the root module AppModule internally assembles two layers: controller and service.
3. High Modularity: AppModule Assembles Controller and Service
How does AppModule "assemble"? Look at app.module.ts; the core is a single @Module decorator:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// Decorator pattern: quickly add behaviors or methods to a class
@Module({
imports: [], // Depend on external modules?
controllers: [AppController], // Controller: validation, simple logic
providers: [AppService], // Data service: complex business logic
})
export class AppModule {}
The instructor clearly divided the responsibilities, which is also the most common MVC approach in backend development (direct quote from main.ts comments):
- Controller: Detects frontend user input, performs parameter validation and simple logic, finally
return response. Corresponds to the C in MVC. - Service (Service/Data Layer): Houses complex business logic, CRUD, SQL, "gives an answer" to the controller layer (comment in app.service.ts). Corresponds to the M (data/model) in MVC.
- View (View Layer HTML): The V in MVC, absent in this hello scaffold—it is a pure backend, not rendering pages.
So "modularity" is implemented as: a Module uses the two arrays controllers and providers to declare which controllers and services it consists of. The framework assembles according to this list at startup.
flowchart TD
M[AppModule] -->|controllers| C[AppController]
M -->|providers| S[AppService]
C -->|calls| S
S -->|returns data| C
Figure 2 clarifies the Module's assembly relationship: the controller is declared in controllers, the service is declared in providers, and the controller then calls the service.
4. Decorator Pattern: How @ Turns an Ordinary Class into a "Framework Component"
Why does an empty class with @Module({...}) get recognized by the framework? This is the decorator pattern: without modifying the original object, dynamically add extra functionality to it. NestJS heavily uses @ decorators to "tag metadata" onto classes; the framework relies on these tags to identify what is a module, what is a controller, and what can be injected. Four types appear in the scaffold:
@Module({...})(app.module.ts): Marks this as a module and declares which controllers/services it contains.@Controller()(app.controller.ts): Marks this as a controller capable of receiving requests.@Get()(app.controller.ts): Marks the method below to handle GET requests (here, the root path/).@Injectable()(app.service.ts): Marks this class as "injectable," able to be obtained by other classes via their constructors.
The most critical part is the dependency injection line:
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { } // Framework injects AppService
@Get()
getHello(): string {
console.log('/ controller');
return this.appService.getHello(); // Response content is delegated to the service layer
}
}
constructor(private readonly appService: AppService) is not you manually calling new; rather, the framework sees @Injectable() and automatically injects an AppService instance. This is why app.module.ts must put AppService into providers—only when declared as a provider can it be injected into the controller. I once got stuck here: I thought this.appService appeared out of thin air, but it is actually the combined result of providers + @Injectable + the constructor working together to complete dependency injection.
5. Factory Pattern: Understanding NestFactory with the Mixue Bingcheng Demo
Design patterns are abstract programming oriented toward interfaces, totaling 23 types; the factory pattern is the first and most important one. The instructor used Mixue Bingcheng as an analogy to explain it thoroughly: if you want milk tea, you don't make it yourself (which would hardcode the process logic); instead, you go to the "factory" Mixue Bingcheng. An enterprise has many products; developers cannot and should not memorize the details of every class, and only need to interact with the factory class: MixueFactory.create(type) is enough. Because every product implements the same show interface, objects produced by the factory can be safely called directly.
Demo code (factory-demo/1.mjs):
class IceCream {
constructor() { this.name = 'Ice Cream'; this.price = 3; }
show() { console.log(`${this.name} ${this.price} yuan`); }
}
class LemonTea {
constructor() { this.name = 'Lemon Tea'; this.price = 4; }
show() { console.log(`${this.name}, ${this.price} yuan`); }
}
class MilkTea {
constructor() { this.name = 'Pearl Milk Tea'; this.price = 8; }
show() { console.log(`${this.name}, ${this.price} yuan`); }
}
// Factory class
class MixueFactory {
static create(type) {
switch (type) {
case 'ice': return new IceCream();
case 'lemon': return new LemonTea();
case 'milk': return new MilkTea();
}
}
}
const drink1 = MixueFactory.create('ice'); drink1.show();
const drink2 = MixueFactory.create('lemon'); drink2.show();
The structure is clear at a glance: the factory uses switch to decide which product to new based on type, and each product implements show().
flowchart LR
F[MixueFactory.create type] --> ICE[IceCream Class]
F --> LEM[LemonTea Class]
F --> MIL[MilkTea Class]
ICE --> SHOW[All implement show interface]
LEM --> SHOW
MIL --> SHOW
Figure 3 explains the factory pattern: the caller only knows MixueFactory.create and does not need to know how the three internal classes are constructed.
Now tie it back to the entry point—the instructor's notes say verbatim "NestFactory Mixue Bingcheng satisfies the need to make an App". NestFactory.create(AppModule) and MixueFactory.create(type) share the same idea: you don't need to manually new up the complex internals of an application/module; hand it to the framework's factory method, and it produces a usable application instance based on the "type" you give (here, the AppModule class). The factory pattern decouples the "caller" from the "diverse classes inside the factory."
One easy mistake: the demo's switch has no default; when type matches no case, create returns undefined, and calling .show() on it will throw an error. In real projects, factories typically need a fallback (return a default product or throw an error); the demo here lacks this and is something to be supplemented.
6. Tracing a Single Request: What Happens at localhost:3000/
Connecting the previous sections, the complete chain when visiting the homepage is:
- The browser requests
localhost:3000/(root route); - The HTTP service started by
listen(3000)in main.ts receives it and, as per the main.ts comment, "sends it to AppModule"; - AppModule's
controllersarray containsAppController, whose@Get()methodgetHello()matches the root path GET; getHello()does not assemble data itself butreturn this.appService.getHello()—delegating to the service layer;AppService.getHello()returns the string'Hello World!';- The string travels back along the original path and becomes the HTTP response body.
sequenceDiagram
participant B as Browser
participant C as AppController
participant S as AppService
B->>C: GET root route
C->>S: this.appService.getHello()
S-->>C: Hello World!
C-->>B: Response string
Figure 4 is the request sequence diagram: the controller only "receives the request and forwards it"; the actual return content is provided by the service layer—this is exactly the MVC division of labor from section three manifested at runtime.
Summary
| Concept | Definition | Key Code |
|---|---|---|
| NestJS | Node pure backend enterprise framework, default TS, fully modular | nest new hello scaffold |
| Entry Point | NestFactory.create(AppModule) creates the app and listens |
main.ts |
| Module | Uses @Module({controllers, providers}) to declare contained controllers and services |
app.module.ts |
| Controller | Receives requests, validates parameters, simple logic, returns response | @Controller() + @Get() |
| Service | Complex business/data, called by controller | @Injectable() + getHello() |
| Decorator | @ tags metadata onto classes; framework identifies and assembles based on this |
@Module/@Controller/@Get/@Injectable |
| Factory Pattern | Caller only knows the factory method, decoupled from concrete product classes | MixueFactory.create(type) |
Common Mistakes and Topics for Further Study
- Dependency Injection Trio: Declaration in
providers+@Injectable()+ constructor parameter; all three are indispensable, otherwisethis.appServicewill beundefined. - Factory Pattern Lacks Fallback:
MixueFactory.create'sswitchhas nodefault; unknowntypereturnsundefined. - MVC in the Scaffold Only Implements C and M (Partially): The instructor's comments mention V (View layer HTML) and a full Model (database), but the hello scaffold has no real database or view; only Controller + Service are visible—this is unimplemented and to be supplemented.
- Parameter Validation Not Demonstrated: The instructor's notes say the controller is responsible for "parameter validation," but
@Get()has no parameters, no Pipe/DTO; the validation syntax is to be supplemented. - Modularity Advanced Topics: The
importsarray (module depending on other modules), middleware, Guards, microservices, concurrency/AI Infra, and other business domains; the instructor listed directions but the code did not cover them—to be supplemented.
Self-Assessment Checklist
- Can clearly state the position of
NestFactory.create(AppModule)in the entire startup flow and map it to the factory pattern. - Upon opening
app.module.ts, can immediately point out whatcontrollersandprovidersare respectively assembling. - Without checking the source code, can describe the complete chain of a
GET /request from the browser to the return of'Hello World!'. - Can explain how
@Injectable()+providers+ the constructor work together to complete dependency injection. - Can retell in your own words what "decoupling the caller from product classes" means in the Mixue Bingcheng demo.
Conclusion: The Three Threads Connected Today
Today, following the NestJS scaffold, we brought together three originally separate threads: at the entry point, NestFactory.create(AppModule) uses the factory pattern to encapsulate the complex task of "creating an application"; structurally, AppModule uses decorators to assemble Controller and Service into a modular whole, with the MVC division of labor ensuring "receiving requests" and "producing data" each have their own roles; mechanistically, @ decorators tag metadata onto ordinary classes that the framework can recognize, cooperating with dependency injection to automatically connect the layers. The next step could be to pick a real business case (a Model with a database + a Controller with parameter validation) and fill in the "unimplemented" parts, moving from a scaffold to a usable backend service.