从 nest new 到 Hello World:一个最小 NestJS 项目,讲透工厂 + 装饰器 + 模块化

NestJS 最小项目全景拆解:NestFactory 工厂、@装饰器、Module 模块


你在浏览器打开 localhost:3000,看到一行 Hello World!

这一行字,是从哪个文件流出来的?你翻开源码,整个项目就 4 个源文件,每个都短得不像话:

text 复制代码
src/
├── main.ts             # 入口
├── app.module.ts       # 根模块
├── app.controller.ts   # 控制器
└── app.service.ts      # 服务

4 个文件,加起来不到 50 行代码。但中间发生了什么、为什么这么拆,网上的教程大多只教你「照抄」,很少一次讲清。

这篇文章把 NestJS 最小项目的骨架彻底拆开,重点不是背 API,而是看懂它背后贯穿始终的三种设计模式,以及「为什么这样设计」。 看完你会知道:NestJS 不是另一个 Express 的写法,它是用「声明式装配」的思路,把后端从脚手架时代带进了框架时代。


先看全景:一行 Hello World 走了四层

text 复制代码
浏览器 GET localhost:3000
        │
        ▼
① main.ts          NestFactory.create(AppModule)   [工厂模式:装配整个应用]
        │
        ▼
② app.module.ts    @Module({ controllers, providers })  [模块化:声明边界]
        │  依赖注入
        ▼
③ app.controller.ts @Controller / @Get()            [装饰器:注册路由]
        │  调用 this.appService.getHello()
        ▼
④ app.service.ts   @Injectable() 返回 'Hello World!'  [服务:业务逻辑]
        │
        ▼
浏览器收到 "Hello World!"

记住这张图,下面每一层都对应一次「为什么要这么设计」。


第一层 main.ts:NestFactory 就是「蜜雪冰城」

整个应用从这一行开始:

typescript 复制代码
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);  // 🔑 工厂方法:一个调用,装配整个应用
  await app.listen(process.env.PORT ?? 3000);        // 启动 HTTP 服务,默认 3000
}
bootstrap();

NestFactory.create(AppModule) 这个调用,背后做的事远不止 new 一个对象:它要创建依赖注入容器、扫描模块、注册路由、装配中间件/管道/守卫/拦截器、初始化生命周期钩子......

这一整套复杂装配,被一个 create() 封装起来了。 这就是工厂模式------你不需要知道「一辆车怎么从零件组装起来」,你只需要跟「4S 店」打交道,说一句「我要一辆车」。

我拿蜜雪冰城打个比方(这是我理解工厂模式的钥匙):

javascript 复制代码
// 工厂类:你只跟它打交道,不用关心内部有多少种产品、怎么做的
class MixueFactory {
  static create(type) {
    switch (type) {
      case 'ice':   return new IceCream();    // 冰激凌 3 元
      case 'lemon': return new LemonTea();    // 柠檬水 4 元
      case 'milk':  return new MilkTea();     // 珍珠奶茶 8 元
    }
  }
}
const drink = MixueFactory.create('lemon');  // 你只要「点单」,不关心泡茶加冰封杯
drink.show();

NestFactory 就是那个蜜雪冰城工厂,AppModule 就是你要的「产品」。工厂模式解决的是「创建过程太复杂、调用方不想关心细节」的问题。


第二层 app.module.ts:模块化 = 声明「我提供什么、我需要什么」

工厂造出来的产品,是一整个 AppModule

typescript 复制代码
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],                        // 🔑 我需要依赖的其它模块
  controllers: [AppController],       // 🔑 我提供的「入口层」(路由、参数校验)
  providers: [AppService],            // 🔑 我提供的「服务层」(业务逻辑、CRUD)
})
export class AppModule {}

一个 @Module 就声明了三件事:我依赖谁(imports)、我暴露哪些控制器(controllers)、我提供哪些服务(providers)。

为什么要把后端拆成模块?因为一个真实的后端动辄几百个类、几十张表,如果全塞在一个文件里,谁也维护不动。模块化就是把「一个 feature」圈成一个边界------用户模块、订单模块、支付模块,各自声明自己的 imports / controllers / providers,互不越界,只通过 imports 显式依赖。

这就是 NestJS 和 Express 最大的分水岭:Express 给你自由(想怎么写都行),NestJS 给你结构(按模块、控制器、服务三件套组织)。


第三层 app.controller.ts:@装饰器 = 给类贴标签

模块声明了控制器,那控制器长什么样?

typescript 复制代码
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()                          // 🔑 告诉 Nest:「这是一个控制器,负责接 HTTP 请求」
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()                               // 🔑 告诉 Nest:「把这个方法注册为 GET / 路由」
  getHello(): string {
    return this.appService.getHello(); // 具体返回什么,交给 service
  }
}

注意 @Controller@Get 这两个 @ 开头的语法------这就是装饰器模式

装饰器模式的本质是:不修改类本身,只给它「贴标签」,让框架根据标签自动做额外的事。 你写了 @Get(),不需要写任何「注册路由」的配置代码,Nest 就自动把这个方法挂到了 GET / 上。

那 Nest 是怎么「看见」这些标签的?靠的是 reflect-metadata------装饰器在类上写入元数据,Nest 启动时扫描这些元数据,决定「这是控制器」「这是 GET 路由」「这个 service 该被注入」。这就是「声明式」:你只声明意图,框架负责落地。

对比一下「命令式」的 Express,你会立刻懂装饰器省了多少事:

javascript 复制代码
// ❌ Express:路由要你自己显式写
app.get('/', (req, res) => res.send('Hello World!'));
typescript 复制代码
// ✅ Nest:@Get 一个注解,路由自动注册
@Get()
getHello(): string { return this.appService.getHello(); }

第四层 app.service.ts:@Injectable 补上依赖注入的最后一环

控制器没自己算「Hello World」,而是调了 this.appService.getHello()。这个 appService 从哪来的?它没被 new 过。

typescript 复制代码
// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()                          // 🔑 标记「这个类可以被注入到别人那里」
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

@Injectable() 把这个类登记进 Nest 的依赖注入(DI)容器。控制器在构造函数里写 constructor(private readonly appService: AppService),Nest 就会自动「看类型,把对应的实例塞进来」------你声明「我要一个 AppService」,框架负责「怎么给」。

这就是依赖注入 的价值:控制器不负责创建服务,只负责声明依赖。好处是解耦 + 可测试------你可以随时把 AppService 换成 mock,控制器一行都不用改。


测试文件里,藏着 DI 的真正好处

翻到 src/app.controller.spec.ts,你会看到 DI 最实在的回报:

typescript 复制代码
const app: TestingModule = await Test.createTestingModule({
  controllers: [AppController],     // 🔑 手动装配:想测哪个,就把哪个装进去
  providers: [AppService],
}).compile();

appController = app.get<AppController>(AppController);

因为控制器不自己 new 服务,所以测试时可以手动指定装配什么 。要测控制器,就装 controller + 一个假 service;要单独测 service,就只装 service。如果当初控制器内部自己 new AppService(),这种「替换」就做不到了。

e2e 测试 test/app.e2e-spec.ts 更是把整个 AppModule 装起来,用 supertest 真实发一个 GET / 请求,断言返回 Hello World!------等于把上面四层链路完整跑了一遍。


三种模式,其实是一件事

拆开看是工厂、装饰器、模块化三个名词,串起来你会发现它们是同一个思想:

NestJS 让你「只声明要什么,不写怎么装配」。

  • 工厂NestFactory.create):把「怎么装配整个应用」藏起来
  • 装饰器@Module / @Controller / @Get / @Injectable):把「我是谁、我要挂在哪」声明出来
  • 模块 + 依赖注入imports / providers):把「谁依赖谁」声明出来,框架自动注入

nest new 出来的 4 个文件,就是这套思想的最小演示:你负责声明结构,框架负责把它跑起来。


结尾:记住「声明式装配」这四个字

回到开头那个问题------localhost:3000 的那行 Hello World,到底走了几层。

它走了四层:main.ts 用工厂造出应用 → app.module.ts 声明模块边界 → app.controller.ts 用装饰器挂上路由 → app.service.ts 被依赖注入、返回数据。

如果只记住一句话,记住这句:

NestJS 的核心不是某一个 API,而是「声明式装配」------你写 @ 声明意图、写 imports/providers 声明依赖,剩下复杂的创建、注入、路由注册,全部交给框架。

下次再看 NestJS 代码,别纠结某个装饰器的写法了。先问自己一句:「这段代码在声明什么?它依赖谁、又被谁依赖?」 顺着 imports → controllers → providers 这条线摸下去,再大的后端项目也能一眼看懂骨架。

留一个开放问题:模块化的反面是「把所有东西写在一个文件里」。你觉得一个后端项目,什么时候该拆模块、什么时候拆得太碎反而过度设计?欢迎在评论区聊聊你的判断标准。


相关推荐
渔夫正在掘金3 小时前
Cordis 中文教程:渐进式构建插件化应用
前端·node.js·ai编程
大家的林语冰6 小时前
✌️ 让 Rust 再次伟大,pnpm 12 抛弃 TypeScript,移植 Rust 原地起飞!
前端·javascript·node.js
抓不住时间的沙8 小时前
butterfly主题美化,打造属于自己的个性博客
java·开发语言·前端·javascript·node.js·github
抓不住时间的沙10 小时前
N1搭建Hexo个人博客,部署到Github
python·docker·node.js·debian·github·arm
必须会一定会11 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
人工智能·node.js·ai编程
艺艺生辉1 天前
从if-else到策略模式
后端·设计模式
小林ixn1 天前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式
八角丶1 天前
Node.js Cluster 详解
前端·node.js
AI人工智能+电脑小能手1 天前
大白话说Java设计模式-17-装饰器模式(源码剖析篇)
java·spring·设计模式·装饰器模式·源码分析·io流