NestJS 快速入门:先别背装饰器,把一条请求跑明白

如果你刚开始学 NestJS,很容易一上来就被这些东西淹没:

Controller、Service、Module、Provider、DTO、Pipe、依赖注入、各种装饰器......

每个词单独看好像都能理解,但放在一起,就不知道它们到底是怎么协作的。

其实快速入门 NestJS,不应该从背概念开始。

最重要的是先搞明白一件事:

前端发来一个 HTTP 请求之后,它在 NestJS 里面到底经历了什么?

只要把这条链路跑通,NestJS 最核心的一套东西基本就串起来了。


一、先建立 NestJS 最重要的心智模型

先不要管数据库、ORM、JWT、RAG、消息队列这些东西。

我们只看一个最简单的请求:

markdown 复制代码
浏览器发出请求
        ↓
Controller:决定哪个方法接收请求
        ↓
Service:执行具体业务逻辑
        ↓
返回结果

除此之外,还有两个重要角色:

css 复制代码
main.ts
  ↓
启动整个 NestJS 应用

Module
  ↓
把 Controller、Service 等组件组织起来

也就是说,刚开始学习 NestJS,只需要先认识四个东西:

css 复制代码
main.ts       启动项目
Module        组织功能
Controller    接收请求
Service       处理业务

这就是整个入门阶段最重要的骨架。


二、一次请求到底是怎么跑起来的?

NestJS 创建出来的默认项目里,一般会看到:

css 复制代码
src/
├── main.ts
├── app.module.ts
├── app.controller.ts
└── app.service.ts

不要急着背这些文件。

我们按照程序真正运行的顺序看。


1. main.ts:程序从这里启动

最关键的是:

ini 复制代码
const app = await NestFactory.create(AppModule);

await app.listen(3000);

第一句:

lua 复制代码
NestFactory.create(AppModule)

可以先简单理解成:

根据 AppModule 的配置,把整个 NestJS 应用创建出来。

第二句:

ini 复制代码
app.listen(3000);

表示:

后端开始监听 3000 端口。

所以当我们访问:

arduino 复制代码
http://localhost:3000

请求才能找到这个 NestJS 服务。


三、Module:不是处理业务,而是负责"组队"

然后看:

less 复制代码
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

初学的时候,不要把 Module 想得太复杂。

它主要是在告诉 NestJS:

sql 复制代码
这个模块有哪些 Controller?
这个模块有哪些 Service?
这个模块还依赖哪些其他 Module?

例如:

ini 复制代码
controllers: [AppController]

表示这个模块里有一个负责接收请求的 AppController。

而:

ini 复制代码
providers: [AppService]

表示:

AppService 交给 NestJS 管理,其他组件可以使用它。

至于:

ini 复制代码
imports: []

则用来引入其他模块。

所以可以先记一句:

Module 不负责具体业务,它负责组织功能。


四、Controller:决定"谁来接这个请求"

来看最简单的 Controller:

less 复制代码
@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
  ) {}

  @Get()
  getHello() {
    return this.appService.getHello();
  }
}

当浏览器访问:

sql 复制代码
GET /

NestJS 会找到:

java 复制代码
@Get()

然后执行下面的方法。

也就是说:

less 复制代码
@Get()
getHello() {}

真正决定请求能不能进入这个方法的,不是 getHello 这个方法名。

而是:

java 复制代码
@Get()

这个路由装饰器。

方法名甚至可以改成:

scss 复制代码
abc()

只要 @Get() 还在,请求照样可以匹配。

所以 Controller 最核心的职责可以理解为:

接请求、拿参数、调用业务逻辑、返回结果。


五、Service:真正干活的地方

例如:

kotlin 复制代码
@Injectable()
export class AppService {
  getHello() {
    return 'Hello World!';
  }
}

Controller:

kotlin 复制代码
@Get()
getHello() {
  return this.appService.getHello();
}

整个过程其实就是:

kotlin 复制代码
GET /
 ↓
AppController
 ↓
appService.getHello()
 ↓
AppService
 ↓
return "Hello World!"
 ↓
浏览器

这也是为什么业务逻辑通常不应该全塞在 Controller 里面。

假设以后你有:

复制代码
上传文件
创建用户
调用大模型
查询数据库
权限检查
文档解析

如果全部写进 Controller,文件很快就会乱掉。

所以一般会形成这样的分工:

复制代码
Controller
负责 HTTP 世界

Service
负责业务世界

六、为什么 Controller 能直接使用 Service?

这里就是 NestJS 非常重要的一个概念:

依赖注入

看这段代码:

typescript 复制代码
constructor(
  private readonly documentService: DocumentService,
) {}

刚开始看很容易疑惑:

documentService 是哪里来的?

你并没有手写:

scss 复制代码
new DocumentService()

原因就在 Module:

kotlin 复制代码
@Module({
  controllers: [DocumentController],
  providers: [DocumentService],
})
export class DocumentModule {}

你已经告诉 NestJS:

复制代码
DocumentService 是这个模块提供的 Provider。

而 Controller 又声明:

复制代码
我需要 DocumentService。

于是 NestJS 创建 Controller 时,就会把 DocumentService 创建好,再传进来。

这就是依赖注入最直观的理解。

可以把它想象成:

markdown 复制代码
Controller:

"我需要一个 DocumentService。"

       ↓

Module:

"我这里登记过 DocumentService。"

       ↓

NestJS:

"行,我创建好以后给你。"

所以入门阶段理解 DI,不需要先研究 IOC 容器的底层原理。

先记住:

需要什么就声明什么,创建和传递依赖这件事交给 NestJS。


七、真正的项目应该按功能拆 Module

一开始所有东西可能都放在:

复制代码
AppController
AppService

但项目一大,这显然不现实。

例如一个知识库项目以后可能有:

复制代码
用户
文档
权限
AI
聊天
搜索

所以我们会拆成:

javascript 复制代码
src/
├── document/
│   ├── document.controller.ts
│   ├── document.service.ts
│   ├── document.module.ts
│   └── dto/
├── user/
├── ai/
└── app.module.ts

例如文档模块:

kotlin 复制代码
@Module({
  controllers: [DocumentController],
  providers: [DocumentService],
})
export class DocumentModule {}

然后在根模块:

kotlin 复制代码
@Module({
  imports: [DocumentModule],
})
export class AppModule {}

于是整个结构逐渐变成:

复制代码
AppModule
   ↓
DocumentModule
   ├── DocumentController
   └── DocumentService

这才是理解 NestJS Module 的关键:

它是按照业务功能划分代码边界的。


八、路由其实就是拼出来的

例如:

kotlin 复制代码
@Controller('documents')
export class DocumentController {
  @Get()
  getDocuments() {}
}

最终接口就是:

bash 复制代码
GET /documents

因为:

less 复制代码
@Controller('documents')
         +
@Get()
         =
GET /documents

如果写:

kotlin 复制代码
@Get(':id')

那么就变成:

bash 复制代码
GET /documents/:id

例如:

bash 复制代码
GET /documents/1
GET /documents/2

所以看到 NestJS 路由时,可以直接把 Controller 前缀和方法路由拼起来理解。


九、@Param:从 URL 中拿数据

假设接口是:

bash 复制代码
GET /documents/123

我们希望拿到 123:

less 复制代码
@Get(':id')
getDocumentById(
  @Param('id', ParseIntPipe) id: number,
) {
  return this.documentService.getDocumentById(id);
}

其中:

kotlin 复制代码
@Get(':id')

表示这里有一个动态参数。

而:

kotlin 复制代码
@Param('id')

就是把它取出来。

不过这里有一个非常重要的细节:

URL 中的:

复制代码
123

本质上拿到的是:

arduino 复制代码
"123"

也就是字符串。

所以光写:

bash 复制代码
id: number

并不会真的把它转换成数字。

TypeScript 类型只存在于开发阶段。

真正负责转换的是:

复制代码
ParseIntPipe

于是流程变成:

arduino 复制代码
"/documents/1"
      ↓
取到 "1"
      ↓
ParseIntPipe
      ↓
1

如果访问:

bash 复制代码
/documents/abc

无法转成数字,Pipe 就会直接返回 400。

这一点非常值得记住:

TypeScript 类型声明不等于运行时数据校验。


十、@Body:接收前端发来的数据

创建文档时可能发送:

bash 复制代码
POST /documents

请求体:

json 复制代码
{
  "title": "请假流程",
  "content": "请提前一天提交申请"
}

Controller 可以这样接:

less 复制代码
@Post()
createDocument(
  @Body() dto: CreateDocumentDto,
) {
  return this.documentService.createDocument(dto);
}

这里:

java 复制代码
@Body()

负责取请求体。

和前面的 @Param() 放在一起看就很清楚:

less 复制代码
@Param()
从 URL 路径拿数据

@Body()
从请求体拿数据

十一、为什么 NestJS 项目经常出现 DTO?

假设用户创建文档时应该发送:

json 复制代码
{
  "title": "员工入职指南"
}

那用户如果发送:

json 复制代码
{
  "title": 123
}

怎么办?

或者:

json 复制代码
{
  "title": "   "
}

怎么办?

这就是 DTO 出现的地方。

DTO 全称:

css 复制代码
Data Transfer Object
数据传输对象

它可以用来描述:

这个接口允许调用者传什么数据,这些数据需要满足什么规则。

例如:

less 复制代码
export class CreateDocumentDto {
  @IsString({
    message: '标题必须是字符串',
  })
  @Matches(/\S/, {
    message: '标题必须包含非空白字符',
  })
  title: string;
}

这里:

java 复制代码
@IsString()

要求必须是字符串。

而:

typescript 复制代码
@Matches(/\S/)

要求字符串中至少存在一个非空白字符。

因此:

json 复制代码
{ "title": "请假流程" }

可以通过。

而:

json 复制代码
{ "title": "   " }

不能通过。


十二、DTO 自己并不会自动检查数据

这是一个很容易踩的坑。

你写了:

makefile 复制代码
dto: CreateDocumentDto

并不代表网络传来的数据就一定符合 CreateDocumentDto。

还需要真正执行校验的人。

也就是:

复制代码
ValidationPipe

例如在 main.ts:

php 复制代码
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);

可以先这样理解:

复制代码
请求进来
   ↓
ValidationPipe
   ↓
按照 DTO 检查
   ↓
合格
   ↓
Controller
   ↓
Service

如果不合格:

复制代码
请求
 ↓
ValidationPipe
 ↓
400

甚至不会进入 Controller。

这就让职责变得很清晰:

复制代码
DTO
描述数据规则

ValidationPipe
执行规则

Controller
接收已经检查过的数据

Service
处理业务

于是 Service 不需要到处写:

csharp 复制代码
if (typeof title !== 'string') {
  ...
}

业务代码会干净很多。


十三、用一个 CRUD 把 NestJS 串起来

理解到这里,可以做一个最小文档模块。

最终有五个接口:

请求 功能
GET /documents 查询列表
GET /documents/:id 查询详情
POST /documents 创建
PATCH /documents/:id 修改
DELETE /documents/:id 删除

这五个接口已经足够把 NestJS 最核心的知识串起来。


查询列表

Controller:

kotlin 复制代码
@Get()
getDocuments() {
  return this.documentService.getDocuments();
}

Service:

javascript 复制代码
getDocuments() {
  return this.documents;
}

流程:

bash 复制代码
GET /documents
       ↓
DocumentController
       ↓
DocumentService
       ↓
documents
       ↓
JSON 响应

查询详情

less 复制代码
@Get(':id')
getDocumentById(
  @Param('id', ParseIntPipe) id: number,
) {
  return this.documentService.getDocumentById(id);
}

Service 找不到数据时:

arduino 复制代码
throw new NotFoundException('文档不存在');

NestJS 会转换成:

复制代码
404 Not Found

于是:

复制代码
参数格式不合法
→ 400

参数合法,但是资源不存在
→ 404

这已经开始接近真正后端接口的处理方式。


创建数据

less 复制代码
@Post()
createDocument(
  @Body() dto: CreateDocumentDto,
) {
  return this.documentService.createDocument(dto);
}

这里你会发现:

Controller 几乎没有业务代码。

它只负责:

css 复制代码
接收 Body
↓
交给 Service
↓
返回结果

这正是比较健康的分工方式。


十四、PATCH 为什么要单独做 Update DTO?

创建文档时:

json 复制代码
{
  "title": "员工手册",
  "content": "正文"
}

标题和正文可能都必须存在。

但修改时,用户可能只想改标题:

json 复制代码
{
  "title": "新版员工手册"
}

正文根本没有修改。

因此创建和修改的数据规则其实并不完全一样。

所以会出现:

复制代码
CreateDocumentDto
UpdateDocumentDto

例如修改 DTO:

less 复制代码
export class UpdateDocumentDto {
  @ValidateIf((_object, value) => value !== undefined)
  @IsString()
  title?: string;

  @ValidateIf((_object, value) => value !== undefined)
  @IsString()
  content?: string;
}

这里:

c 复制代码
title?: string

表示标题可以不传。

而:

java 复制代码
@ValidateIf(...)

表示:

传了我才校验。

于是:

复制代码
{}

和:

json 复制代码
{
  "title": null
}

并不是一回事。

前者是:

复制代码
我不修改 title。

后者是:

csharp 复制代码
我要把 title 改成 null。

业务语义完全不同。


十五、DELETE:成功不一定非要返回一句"删除成功"

例如:

less 复制代码
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteDocument(
  @Param('id', ParseIntPipe) id: number,
) {
  this.documentService.deleteDocument(id);
}

删除成功返回:

css 复制代码
204 No Content

意思就是:

操作完成,但是没有响应正文。

而如果目标文档不存在:

复制代码
404 Not Found

于是一个小型文档 CRUD 就完整了。


十六、现在重新看一条请求

学完前面的东西以后,再看:

bash 复制代码
POST /documents

你应该能把它还原成:

bash 复制代码
客户端
  ↓
HTTP POST /documents
  ↓
ValidationPipe
  ↓
CreateDocumentDto 校验
  ↓
DocumentController
  ↓
DocumentService
  ↓
创建数据
  ↓
Controller 返回结果
  ↓
NestJS 转成 HTTP 响应
  ↓
客户端

到这里,NestJS 就不再是一堆零散装饰器了。

而是一条完整的数据流。


十七、NestJS 最核心的几个装饰器其实没有那么复杂

快速过一遍:

kotlin 复制代码
@Controller('documents')

这个类负责 /documents 相关请求。

java 复制代码
@Get()

接 GET。

java 复制代码
@Post()

接 POST。

kotlin 复制代码
@Patch(':id')

接 PATCH。

kotlin 复制代码
@Delete(':id')

接 DELETE。

kotlin 复制代码
@Param('id')

从 URL 路径拿参数。

java 复制代码
@Body()

从请求体拿数据。

java 复制代码
@Injectable()

告诉 Nest:

这个类可以交给依赖注入系统管理。

java 复制代码
@Module()

告诉 Nest:

这个模块有哪些 Controller、Provider 和其他 Module。

如果把这些装饰器放回真实请求流程里理解,它们其实一点也不神秘。


十八、初学 NestJS 最容易混淆的几个地方

1. 不要认为 id: number 会自动校验参数

不会。

bash 复制代码
id: number

主要是 TypeScript 类型信息。

运行时需要:

复制代码
ParseIntPipe

2. 不要把 DTO 当数据库表

DTO 描述的是:

复制代码
接口传输的数据

数据库 Entity 描述的是:

复制代码
数据如何存储

它们可能长得很像,但职责不同。


3. 不要把业务全塞进 Controller

更好的结构是:

复制代码
Controller
接收 HTTP 请求

Service
处理业务

4. 不要只创建 Module,却忘记导入它

文件顶部:

javascript 复制代码
import { DocumentModule } from './document/document.module';

只是让这个文件能够使用 DocumentModule 这个类。

真正把它加入 NestJS 应用,还需要:

css 复制代码
@Module({
  imports: [DocumentModule],
})

这两个 import 不是一回事。


5. 不要一开始死磕依赖注入底层

刚开始只需要理解:

typescript 复制代码
constructor(
  private readonly documentService: DocumentService,
) {}

表示:

复制代码
Controller 需要 DocumentService。

而:

ini 复制代码
providers: [DocumentService]

表示:

复制代码
NestJS 可以提供这个 Service。

先会用,再研究 DI Token、useFactory、useValue 等更高级的东西。


十九、快速入门 NestJS,到什么程度算真正入门?

不是背出:

sql 复制代码
Controller 是控制器
Service 是服务
Module 是模块

这没有太大意义。

真正入门的标准应该是:

看到:

bash 复制代码
PATCH /documents/123

你脑子里能够自然还原:

bash 复制代码
请求进入 NestJS
↓
根据路由找到 Controller
↓
ParseIntPipe 处理 id
↓
ValidationPipe 根据 DTO 校验 Body
↓
Controller 调用 Service
↓
Service 修改数据
↓
结果返回客户端

如果这条链路你能说清楚,那么 NestJS 最核心的骨架其实已经掌握了。

后面学习 PostgreSQL、TypeORM、JWT、文件上传、Redis,甚至把 LangChain、Agent、RAG 接进 NestJS,本质上都只是在这套骨架上继续增加能力。


总结

如果只用一句话概括 NestJS:

NestJS 就是在帮我们把"请求怎么进来、业务放哪里、功能怎么组织、依赖怎么连接、数据怎么校验"这几件事规范起来。

快速入门时,不需要先背几十个装饰器。

先把这条主线吃透:

css 复制代码
main.ts
  ↓
Module
  ↓
Controller
  ↓
Pipe / DTO
  ↓
Service
  ↓
返回结果

然后亲手做一套:

sql 复制代码
GET
GET /:id
POST
PATCH
DELETE

NestJS 的第一层就基本打通了。

再往后,无论接数据库还是接 AI,你都已经知道:新的代码应该放在哪里,以及一次请求到底是怎么流过整个后端的。

相关推荐
晴空蓝天1 小时前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python
明月_清风1 小时前
干了 6 年前端,我是怎么一步步转型到 AI 的?
前端·后端·ai编程
顽疲1 小时前
Java vue 养老系统源码详解:楼宇床位 care_space 与房态图 Spring Boot 实践
后端
Lyy1 小时前
DevOps平台 — 第十三篇:规划的设计与实现
后端·devops
今天不在线2 小时前
旧 Java 项目接入 AI 实战系列(一):四层递进,从基础对话到流式输出
后端
卷无止境2 小时前
WebGIS生态全景丨从浏览器里的地图到背后的空间数据库
后端·python
JuiceFS2 小时前
JuiceFS 企业版 5.4:从千亿文件到百万客户端
后端
Lost of 程序猿2 小时前
命令模式实战:把“下单后的动作“打包成可执行的任务
后端·设计模式·c#·asp.net·命令模式
焦玉全2 小时前
Sharding-JDBC 分库分表实战:从 800 万订单表的查询优化说起
后端