跳到主要内容

创建日期:2026-09-08 | 最近更新:2026-09-08 事实核对基于 NestJS 12@nestjs/core 12.0.1 / @nestjs/cli 12.0.0,Node ≥ 20)在 本机 Node 24 实测:脚手架为 npx @nestjs/cli 生成,下方启动日志与 curl 返回均为真实运行输出。

NestJS 入门指南:Node 界的“Spring”

一句话:NestJS 是 TypeScript 写的后端框架,把 Angular 的模块化 + 依赖注入 + 装饰器那套架构搬到了服务端。它不像 Express 那样“给个函数自己拼”,而是用模块(Module)→ 控制器(Controller)→ 提供者(Provider) 的结构把应用组织起来——如果你刚看完本站的 Spring 入门,会强烈地感到“这俩是同一个爹”:

概念Spring(Java)NestJS(TS)
装饰器声明 Bean@Component / @Service@Injectable()
HTTP 控制器@RestController@Controller()
依赖注入构造器注入构造器注入(几乎一模一样)
组装模块@Configuration / 包@Module({ controllers, providers, imports })

两边都信一句话:new,让容器把依赖给你。

本文定位

本系列第 0 篇(入门),目标:装好 → 看懂脚手架 → 跑起第一个接口 → 看懂 Controller/Provider/Module 怎么协作,让你对“Nest 到底长什么样”有实感(本文代码已在本机跑通,输出见 §5)。

系列目录(全部完成):

主题状态
0入门:装好 + 解剖脚手架 + 第一个接口(本文
1模块化与 Provider:作用域、生命周期、自定义 provider
2数据层:TypeORM / Prisma 怎么选、怎么接进 Nest
3DTO / ValidationPipe:把参数校验做成规范(实测)
4鉴权 Guard / JWT / RBAC(实测)
5测试与部署:e2e、Docker、生产 checklist

1. 为什么不是“又一个 Express 封装”

Express 的问题是:太自由。路由散落、无结构约定、鉴权/校验各写各的——项目一大人人难受。Nest 提供的是架构

  • TypeScript 一等公民:装饰器描述路由与元数据,类型贯穿到 DTO 校验;
  • 模块化:每个业务域一个 @Module,可复用、可测试、可 lazy;
  • DI 容器:构造器注入,跟 Spring 同款心智;
  • 平台无关:默认跑在 Express 上,一个 NestFactory.create(AppModule, new FastifyAdapter()) 就能切 Fastify;
  • 同款全家桶:CLI 生成、内置 ValidationPipe、Guard/Interceptor/Pipe、WebSocket、微服务、GraphQL 都有官方模块。

代价(诚实说):样板和抽象比 Express 重,写超小工具杀鸡用牛刀;适合的是“要长期维护的正式服务”。

2. 环境与安装

要求
Node≥ 20(Nest 12;CLI 要求 ≥ 20.11)
包管理器npm / yarn / pnpm 均可
TSCLI 生成,无需手配
node -v # 本机:v24.14.1
npx --yes @nestjs/cli@12 new my-app -p npm --skip-git
cd my-app
npm run start:dev # watch 模式开发

CLI 也可以只装全局:npm i -g @nestjs/cli。生成时 -p npm 指定包管理器,避免交互提问。

3. 解剖脚手架(Nest 12 生成的工程长这样)

Nest 12 默认项目是 ESM + NodeNext 风格——注意源码里 import 都带 .js 后缀(./app.service.js),main.ts顶层 await 直接启动,别按老 CJS 习惯手删后缀。

my-app/
├─ package.json
├─ nest-cli.json # CLI 配置(sourceRoot、compilerOptions)
├─ tsconfig.json
├─ src/
│ ├─ main.ts # 入口:NestFactory.create → listen
│ ├─ app.module.ts # 根模块:把 controllers/providers 组装起来
│ ├─ app.controller.ts # 控制器:路由 + HTTP 方法装饰器
│ ├─ app.controller.spec.ts # 单元测试
│ └─ app.service.ts # 提供者:业务逻辑(@Injectable)
└─ test/
└─ app.e2e-spec.ts # e2e 测试(supertest)

生成的最小 package.json 依赖(就这几个,不臃肿):

{
"dependencies": {
"@nestjs/common": "^12.0.1", // 装饰器、Controller/Module/Injectable
"@nestjs/core": "^12.0.1", // 容器与启动
"@nestjs/platform-express": "^12.0.1",// HTTP 平台适配器(Express)
"reflect-metadata": "^0.2.2", // 装饰器元数据运行时
"rxjs": "^7.8.1" // 响应式(Nest 内部依赖)
}
}

三个核心文件对照着看,就是 Nest 的最小世界观:

// app.module.ts —— 组装一切的地方
@Module({
imports: [], // 引入其它模块
controllers: [AppController], // 本模块的控制器
providers: [AppService], // 本模块的提供者(可被注入)
})
export class AppModule {}
// app.controller.ts —— 只做「路由 → 调 service」
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {} // ← DI:容器把 service 注入进来

@Get()
getHello(): string {
return this.appService.getHello();
}
}
// app.service.ts —— 业务逻辑(对 controller 而言只是“可注入的依赖”)
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

对照 Spring:@Module(providers) ≈ Spring 容器里的 Bean 声明,@Injectable()@Service@Controller()@RestController,构造器注入更是原封不动。看懂了 Spring 那一篇,Nest 的代码九成不用教。

4. 动手:加一个带参数的接口

在脚手架上加一个 GET /hello?name=Lin(改成 app.controller.ts + app.service.ts 各加一个方法):

// app.controller.ts(新增)
import { Controller, Get, Query } from '@nestjs/common';
// …
@Get('hello') // 路由前缀拼接:@Controller() 空 + /hello
greet(@Query('name') name: string = 'world'): { message: string } {
return this.appService.greet(name);
}
// app.service.ts(新增)
greet(name: string): { message: string } {
return { message: `Hello, ${name}!` };
}

5. 跑起来(本机实测输出)

npm run build # 产物在 dist/
node dist/main.js # 或 npm run start

启动日志(真实输出,注意它把每个路由都“Mapped”出来了——这就是装饰器元数据被框架扫描的痕迹):

[Nest] 65830 - 2026/09/08 08:42:38 LOG [NestFactory] Starting Nest application...
[Nest] 65830 - 2026/09/08 08:42:38 LOG [InstanceLoader] AppModule dependencies initialized +7ms
[Nest] 65830 - 2026/09/08 08:42:38 LOG [RoutesResolver] AppController {/}: +7ms
[Nest] 65830 - 2026/09/08 08:42:38 LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 65830 - 2026/09/08 08:42:38 LOG [RouterExplorer] Mapped {/hello, GET} route +1ms
[Nest] 65830 - 2026/09/08 08:42:38 LOG [NestApplication] Nest application successfully started

实测请求(默认端口 3000,可用 PORT 环境变量改):

$ curl http://localhost:3000/
Hello World! ← 默认 GET /

$ curl "http://localhost:3000/hello?name=Lin"
{"message":"Hello, Lin!"} ← @Query 读到 name

$ curl http://localhost:3000/hello
{"message":"Hello, world!"} ← 缺省值 world 生效

三个观察点:

  1. 返回对象会被自动 JSON 化(默认 Express 序列化),所以 service 返回 { message } 直接就是 JSON;
  2. @Query('name') name: string = 'world' —— 装饰器把 query 参数按名字取,TS 默认值兜底;
  3. 启动日志里 AppModule dependencies initialized 说明容器已经创建了 AppService 并注入给了 AppController——你全程没写过一个 new AppService()

6. 常用 CLI(生成即架构)

npx nest --help
npx nest g controller cats # 生成 controller(可带目录:module/cats)
npx nest g service cats
npx nest g module cats # 每个业务域一个 module,再 controller/service 挂进去
npx nest g resource cats # 一把梭:module+controller+service+DTO+e2e(CRUD 模板)

nest g resource 尤其适合 REST 起步——它把「controller → service → DTO」整套 CRUD 都生成好,省去手动接线。

7. 进阶路径(本系列已全部补齐)

入门到能跑之后,按下面顺序把服务做成「正式形态」(每篇都已是完整文章):

  • Provider 与作用域:为什么 service 默认单例、@Injectable 作用域、自定义 provider → 篇 1
  • 数据层:TypeORM / Prisma 各自的 Nest Module 接法 → 篇 2
  • 校验:class-validator + ValidationPipe,把 DTO 变成声明式校验 → 篇 3
  • 鉴权Guard + JWT → 篇 4
  • 收尾:e2e 测试、nest build 产物、Docker → 篇 5

参考