NestFactory Is a Factory: How Nest.js Bootstraps from a Single create() Call
Factory Pattern and the Core Ideas of Nest.js — From Mixue Ice Cream & Tea to Enterprise Architecture
Through the real story of Mixue Ice Cream & Tea, this article helps you understand the design philosophy of the factory pattern, and uses it to dive into Nest.js modules, controllers, services, dependency injection, and decorators, so you can thoroughly grasp how a Nest application "comes to life."
1. Why Do We Need Design Patterns? Starting from "Ordering Milk Tea"
A note says:
"If you want milk tea, you don't make it yourself (no procedural code). You go to Mixue."
This is actually a simple embodiment of interface-oriented programming. As consumers, we don't care about the milk tea production process (how much tea, how much sugar); we only need to know that the factory called "Mixue" can provide the product we want.
In the code world, if we directly use new IceCream(), new LemonTea(), every place that uses them must be modified whenever a new product is added. This is tight coupling.
The Factory Pattern emerged to solve this: encapsulate object creation logic into a factory class. The user only needs to tell the factory "what type I want," and the factory returns the corresponding instance. This is exactly what the MixueFactory code in the note demonstrates.
class MixueFactory {
static create(type) {
switch(type) {
case 'ice': return new IceCream();
case 'lemon': return new LemonTea();
case 'milk': return new MilkTea();
}
}
}
Benefits this brings:
- The user (developer) only needs to remember
create('ice'), without knowing the internal details of theIceCreamclass. - If the product construction method changes in the future (e.g., adding parameters), only the factory class needs modification; all call sites remain unchanged.
- All product classes implement the same
show()method, ensuring behavioral consistency.
Key sentence from the note:
"Developers only need to call
MixueFactory.create('type'). Since every class in the factory implements the same show interface, classes produced by the factory can be called directly with confidence."
2. The Factory Pattern in Nest.js: NestFactory
In Nest.js, we see this code in main.ts:
const app = await NestFactory.create(AppModule);
NestFactory is a factory class responsible for creating the entire Nest application instance. We don't need to care about how it internally initializes middleware or configures the underlying HTTP server (Express/Fastify); we just need to pass in the root module AppModule to get a runnable application object.
This perfectly corresponds to the Mixue story:
- Mixue factory →
NestFactory - Ordering a lemonade →
NestFactory.create(AppModule) - Factory returns a product instance → Returns the
appobject, and we can callapp.listen(3000)
3. Nest's Modularity — Decoding the @Module Decorator
The note says:
"A Module is a whole, the most common backend MVC pattern. A file with thousands of lines of code organizes the controller and service layer CRUD."
Nest uses the @Module() decorator to organize controllers and services into a logical unit. Let's look at the app.module.ts code:
@Module({
imports: [], // Depend on the outside world?
controllers: [AppController], // Controller: validation, simple logic
providers: [AppService], // data service: complex business logic
})
export class AppModule {}
In-depth interpretation of the comments:
imports: [] // Depend on the outside world?This array is used to import other modules. For example, we will later importTodosModule, meaning this module needs to use services provided by other modules. It's like "I want to borrow personnel from the neighboring department"; only if the imported module has exposed services in itsexportscan the current module use them.controllers: [AppController] // Controller: validation, simple logicThe controller is only responsible for "receiving guests" — receiving requests, extracting parameters, calling services, and returning responses. "Validation" is strictly handled by theValidationPipe; the controller itself doesn't write validation logic, but it is the "entry point" for validation. "Simple logic" means the controller must not contain complex business calculations; it only does route dispatching.providers: [AppService] // data service: complex businessRegistered here are all@Injectable()classes, which are responsible for real business logic, database operations, and complex calculations. This is the Service layer, the embodiment of the Model in MVC, though Nest prefers to call it a Provider.
4. Controllers and Services — The Magic of Dependency Injection
In app.controller.ts:
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// ...
}
The underlying logic of this line of code:
- When TypeScript compiles, the type information of the
constructorparameters is preserved in metadata (design:paramtypes). - At startup, Nest scans all
@Controller()classes and discovers thatAppControllerneedsAppService. - It checks the IoC container to see if an instance of
AppServicealready exists (sinceAppServiceis marked with@Injectable()and registered inproviders). - If it exists, it is injected directly; if not, it is created first and then injected.
- After injection,
this.appServicecan be used directly.
This is the essence of Dependency Injection (DI): Inversion of Control — instead of us actively using new, the container "delivers" the dependency to us. This greatly reduces coupling and facilitates unit testing (mock objects can be injected).
5. The Decorator Pattern — What Exactly Is the @ Symbol?
The note says:
"The decorator pattern dynamically adds extra functionality to an object without modifying the original object."
But @Controller, @Module, @Injectable in Nest are not the classic decorator pattern (runtime wrapping). They are TypeScript decorators, essentially a higher-order function that executes when the class is defined, used to add metadata to the class.
For example, @Module({...}) attaches configuration information to the AppModule class. Nest reads this metadata at startup to know how to assemble the application.
The real application of the decorator pattern in Nest is Interceptors and Guards, which wrap route handler functions at runtime, dynamically adding features like logging and permissions. We will dive into this part in a follow-up article.
6. Understanding the Application Startup Process from main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Process breakdown:
NestFactory.create(AppModule)— The factory creates the application instance.- Internally, it reads the metadata of
AppModuleand recursively resolves all dependent modules (likeTodosModule). - It initializes all controllers and services, and establishes route mappings.
- It starts the underlying HTTP server, listening on the specified port.
- At this point, our API is accessible.
7. Summary
| Concept | Corresponding Note/Code | My Understanding |
|---|---|---|
| Factory Pattern | Mixue, NestFactory | Encapsulates object creation, decouples caller from implementation class |
| Module | @Module({imports, controllers, providers}) | A unit for organizing code, like a company department |
| Controller | @Controller, responsible for routing and parameter extraction | Only does dispatching, no business logic |
| Service | @Injectable, responsible for business logic | Can be injected anywhere needed |
| Dependency Injection | constructor(private readonly xxxService) | Container automatically provides dependencies, no manual new needed |
| Decorator | @ symbol, provides metadata | Tags classes/methods for the framework to read |