← 返回AI变现
🌐 其他

NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程

来源:掘金 · 发布于 2026-08-19 12:16:26
NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程 一、NestJS 开发流程全景 1.1 从单模块到多模块的演进 1.2 NestJS 模

NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程

Darling噜啦啦 2026-08-19 0 阅读13分钟

NestJS 全栈 CRUD 实战:从 Module 拆分到 RESTful 七大装饰器,彻底搞懂后端接口工程

上篇我们用蜜雪冰城理解了工厂模式和装饰器模式,掌握了 NestJS 的设计哲学。这篇进入实战——用 NestJS 从零搭建一个完整的 Todos CRUD 接口。从根模块拆分子模块,到 Controller 的五大 HTTP 装饰器、Service 的业务逻辑与错误处理,再到 @Param @Body 参数提取和 Partial<T> 类型技巧——一篇文章打通后端接口工程的完整链路。全文代码可直接运行,建议收藏后动手实践。


一、NestJS 开发流程全景

1.1 从单模块到多模块的演进

上篇:单模块架构(学习阶段)
  src/
  ├── main.ts                # 入口
  ├── app.module.ts          # 根模块(所有功能堆在一起)
  ├── app.controller.ts      # 根控制器
  └── app.service.ts         # 根服务

本篇:多模块架构(企业级)
  src/
  ├── main.ts                # 入口
  ├── app.module.ts          # 根模块(imports 子模块)
  ├── app.controller.ts      # 根控制器
  ├── app.service.ts         # 根服务
  └── todos/                  # Todos 业务模块
      ├── todos.module.ts     # 模块定义(组装 Controller + Service)
      ├── Todos.controller.ts # 控制器(路由 + 参数校验)
      └── Todos.service.ts    # 服务(业务逻辑 + 数据操作)

1.2 NestJS 模块开发约定

┌──────────────────────────────────────────────────────────┐
│                NestJS 模块开发流程                         │
│                                                          │
│  ① AppModule 的 imports 中植入业务模块                    │
│     → @Module({ imports: [TodosModule] })                │
│                                                          │
│  ② 每个业务模块是独立的 MVC 单元                           │
│     → xx.module.ts    定义模块,组装 Controller + Service │
│     → xx.controller.ts 控制器,处理 HTTP 请求              │
│     → xx.service.ts   服务层,处理业务逻辑                 │
│                                                          │
│  ③ Service 用 @Injectable() 标记                          │
│     → 自动依赖注入到 Controller                           │
│     → Controller 构造函数中声明依赖                        │
│     → 不需要手动 new,NestJS DI 容器管理                   │
│                                                          │
│  ④ Controller 不直接操作数据库                             │
│     → 通过 Service 间接操作                               │
│     → MVC 分层:View(Controller) → Model(Service)        │
└──────────────────────────────────────────────────────────┘

1.3 RESTful API 设计

Todos 接口设计(RESTful 风格):

  HTTP 方法    路径              功能        NestJS 装饰器
  ──────────────────────────────────────────────────────
  GET         /todos            获取所有     @Get()
  GET         /todos/:id       获取单个     @Get(':id')
  POST        /todos            创建         @Post()
  DELETE      /todos/:id        删除         @Delete(':id')
  PATCH       /todos/:id        部分更新     @Patch(':id')

  RESTful 核心:
  ├── 用 HTTP 方法区分操作类型(GET/POST/DELETE/PATCH)
  ├── 用 URL 路径定位资源(/todos/:id)
  ├── 用 HTTP 状态码表达结果(200/201/404/204)
  └── 用 JSON 作为数据格式

二、根模块:AppModule 植入子模块

2.1 app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';

@Module({
  imports: [TodosModule],           // 植入 Todos 业务模块
  controllers: [AppController],     // 根控制器
  providers: [AppService],          // 根服务
})
export class AppModule {}

关键变化:

之前(单模块):
  @Module({
    imports: [],                    // 没有子模块
    controllers: [AppController],
    providers: [AppService],
  })

现在(多模块):
  @Module({
    imports: [TodosModule],         // ← 植入业务模块
    controllers: [AppController],
    providers: [AppService],
  })

  imports 的作用:
  → 告诉 AppModule "我依赖 TodosModule"
  → NestJS 启动时会自动加载 TodosModule
  → TodosModule 中的 Controller 和 Service 会被注册
  → 路由 /todos 会被激活
模块依赖关系图:

  AppModule(根模块)
    │
    ├── imports: [TodosModule]  ← 植入
    │     │
    │     ├── controllers: [TodosController]  → 路由 /todos
    │     └── providers: [TodosService]       → 业务逻辑
    │
    ├── controllers: [AppController]  → 路由 /
    └── providers: [AppService]       → 根服务

三、模块定义:TodosModule 的组装

3.1 todos.module.ts

import { Module } from '@nestjs/common';
import { TodosController } from './Todos.controller';
import { TodosService } from './Todos.service';

@Module({
  controllers: [TodosController],   // 注册控制器
  providers: [TodosService],        // 注册服务(可被注入)
})
export class TodosModule {}

模块的职责:

TodosModule 就是一个"装配车间":

  ┌──────────────────────────────────────────────┐
  │  TodosModule(装配车间)                      │
  │                                              │
  │  controllers: [TodosController]              │
  │  → 注册控制器,激活 /todos 路由                │
  │                                              │
  │  providers: [TodosService]                   │
  │  → 注册服务,放入 DI 容器                      │
  │  → TodosController 需要时自动注入              │
  │                                              │
  │  模块不写业务逻辑,只负责"组装"                 │
  └──────────────────────────────────────────────┘

MVC 分层原则:

  View层(Controller)
    → 不可以直接去数据库查数据
    → 只接收请求、校验参数、调用 Service、返回响应

  Model层(Service)
    → 处理业务逻辑
    → 数据库 CRUD
    → 数据处理与转换

  NestJS 的 MVC:
    V = Controller(视图层 = JSON 响应)
    C = Controller 中的路由逻辑
    M = Service + 数据库

四、控制器层:五大 HTTP 装饰器

4.1 Todos.controller.ts 完整代码

import {
  Controller,
  Get,
  Post,
  Delete,
  Patch,
  Param,
  Body,
} from '@nestjs/common';
import { TodosService } from './Todos.service';
import type { Todo } from './Todos.service';

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  // GET /todos → 获取所有
  @Get()
  findAll(): Todo[] {
    return this.todosService.findAll();
  }

  // GET /todos/:id → 获取单个
  @Get(':id')
  findOne(@Param('id') id: string): Todo {
    return this.todosService.findOne(Number(id));
  }

  // POST /todos → 创建
  @Post()
  create(@Body('title') title: string): Todo {
    return this.todosService.create(title);
  }

  // DELETE /todos/:id → 删除
  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: 'success' };
  }

  // PATCH /todos/:id → 部分更新
  @Patch(':id')
  update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
    return this.todosService.update(Number(id), patch);
  }
}

4.2 类装饰器:@Controller('todos')

@Controller('todos')
export class TodosController { ... }
@Controller('todos') 的作用:

  → 给控制器设置路由前缀 'todos'
  → 控制器内所有路由都自动加上 /todos 前缀

  @Get()        → GET /todos
  @Get(':id')   → GET /todos/:id
  @Post()       → POST /todos
  @Delete(':id') → DELETE /todos/:id
  @Patch(':id') → PATCH /todos/:id

  没有 @Controller('todos') 的话:
  @Get()        → GET /          ← 路径冲突
  @Get(':id')   → GET /:id       ← 和其他控制器冲突

  路由前缀让多个控制器各管各的资源,互不冲突

4.3 方法装饰器:五大 HTTP 方法

@Get()          // GET    → 查询资源
@Get(':id')     // GET    → 查询单个资源
@Post()         // POST   → 创建资源
@Delete(':id')  // DELETE → 删除资源
@Patch(':id')   // PATCH  → 部分更新资源
HTTP 方法与 CRUD 的对应关系:

  C(Create)   → POST   → 创建新资源
  R(Read)     → GET    → 查询资源
  U(Update)   → PATCH  → 部分更新(只改传了的字段)
                → PUT   → 全量更新(替换整个资源)
  D(Delete)   → DELETE → 删除资源

  PATCH vs PUT 的区别:
  PATCH /todos/1  { "complete": true }
  → 只改 complete 字段,title 不变

  PUT /todos/1  { "title": "新标题", "complete": true }
  → 整个替换,必须传所有字段
NestJS 支持的 HTTP 方法装饰器:

  @Get()        → GET     查询
  @Post()       → POST    创建
  @Put()        → PUT     全量更新
  @Patch()      → PATCH   部分更新
  @Delete()     → DELETE  删除
  @All()        → 所有方法 都匹配
  @Head()       → HEAD    只获取头信息
  @Options()    → OPTIONS 预检请求

4.4 参数装饰器:@Param 和 @Body

// @Param('id') → 从 URL 路径中提取参数
@Get(':id')
findOne(@Param('id') id: string): Todo {
  return this.todosService.findOne(Number(id));
}
// 请求 GET /todos/5
// @Param('id') → id = '5'(注意:URL 参数永远是 string)

// @Body('title') → 从请求体中提取指定字段
@Post()
create(@Body('title') title: string): Todo {
  return this.todosService.create(title);
}
// 请求 POST /todos
// Body: { "title": "学习 NestJS" }
// @Body('title') → title = '学习 NestJS'

// @Body() → 提取整个请求体
@Patch(':id')
update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
  return this.todosService.update(Number(id), patch);
}
// 请求 PATCH /todos/1
// Body: { "complete": true }
// @Body() → patch = { complete: true }
NestJS 参数装饰器全家桶:

  @Param('id')     → URL 路径参数   /todos/:id → id
  @Body('title')   → 请求体指定字段  { title: 'xxx' } → title
  @Body()          → 整个请求体      { title, complete } → 整个对象
  @Query('page')   → 查询参数       /todos?page=1 → page
  @Headers('auth') → 请求头指定字段  Authorization: Bearer xxx
  @Req()           → 整个 Request 对象
  @Res()           → 整个 Response 对象

  参数装饰器的价值:
  → 声明式获取请求参数,不需要手动解析
  → TypeScript 类型标注,编译时检查
  → 只取需要的字段,不引入整个 Request 对象

4.5 依赖注入:构造函数注入 Service

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}
  //            │        │        │
  //            │        │        └── 类型:TodosService
  //            │        │            → NestJS 根据类型从 DI 容器找实例
  //            │        └── readonly:只读,防止在控制器中修改 Service
  //            └── private:私有属性,类外部不可访问

  // 注入后直接使用
  @Get()
  findAll(): Todo[] {
    return this.todosService.findAll();
    //     └── 不需要手动 new TodosService()
    //         NestJS 自动创建并注入实例
  }
}

依赖注入的完整流程:

┌──────────────────────────────────────────────────────────┐
│              依赖注入(DI)完整流程                       │
│                                                          │
│  1. TodosService 类被 @Injectable() 标记                 │
│     → "我是一个可被注入的服务"                            │
│                                                          │
│  2. TodosModule 的 providers 注册了 TodosService          │
│     → NestJS DI 容器创建并管理 TodosService 实例          │
│                                                          │
│  3. TodosController 构造函数声明需要 TodosService         │
│     constructor(private readonly todosService: TodosService)│
│     → NestJS 看到类型是 TodosService                     │
│     → 从 DI 容器中取出实例                               │
│     → 自动注入到构造函数参数                             │
│                                                          │
│  4. 控制器中直接 this.todosService.findAll()             │
│     → 不关心实例怎么来的,只管用                          │
│                                                          │
│  这就是"控制反转"(IoC):                               │
│  对象的创建控制权从开发者转移到了框架                     │
└──────────────────────────────────────────────────────────┘

五、服务层:业务逻辑与错误处理

5.1 Todos.service.ts 完整代码

import {
  Injectable,
  NotFoundException,
} from '@nestjs/common';

// 数据模型接口
export interface Todo {
  id: number;
  title: string;
  complete: boolean;
}

// 内存数据源(实际项目中替换为数据库)
let todos: Todo[] = [
  { id: 1, title: '学习 NestJS', complete: false },
  { id: 2, title: '学习 CRUD', complete: true },
];

let nextId = 3;  // 自增 ID

@Injectable()
export class TodosService {
  // 查询所有
  findAll(): Todo[] {
    return todos;
  }

  // 查询单个
  findOne(id: number): Todo {
    const todo = todos.find(t => t.id === id);
    if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
    return todo;
  }

  // 创建
  create(title: string): Todo {
    const todo: Todo = { id: nextId++, title, complete: false };
    todos.push(todo);
    return todo;
  }

  // 删除
  remove(id: number): void {
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) throw new NotFoundException(`Todo ${id} 不存在`);
    todos.splice(index, 1);
  }

  // 部分更新
  update(id: number, patch: Partial<Todo>): Todo {
    const todo = todos.find(t => t.id === id);
    if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
    Object.assign(todo, patch);
    return todo;
  }
}

5.2 数据模型设计

export interface Todo {
  id: number;          // 唯一标识
  title: string;       // 任务标题
  complete: boolean;   // 是否完成
}
TypeScript interface 的特点:

  interface Todo { ... }
  → 只描述数据结构,编译后会被完全移除
  → 不产生运行时代码
  → 用于类型检查,不占运行时体积

let todos: Todo[] = [ ... ]
let nextId = 3;

→ 用 let 而非 const:数据需要增删改
→ nextId 自增 ID 生成器
→ 实际项目中用数据库的自增 ID

5.3 五大业务方法逐个拆解

① findAll():查全部

findAll(): Todo[] {
  return todos;
}
// 直接返回整个数组
// 实际项目中会加分页、过滤、排序

② findOne(id):查单个 + 错误处理

findOne(id: number): Todo {
  const todo = todos.find(t => t.id === id);
  if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
  return todo;
}
// find 找不到返回 undefined,不是报错
// 需要手动检查并抛出 NotFoundException
// NestJS 会把 NotFoundException 转成 HTTP 404 响应

③ create(title):创建

create(title: string): Todo {
  const todo: Todo = { id: nextId++, title, complete: false };
  todos.push(todo);
  return todo;
}
// nextId++ → 先用当前值,再自增
// 新任务默认 complete: false(未完成)
// 返回创建的 todo(包含分配的 id)

④ remove(id):删除

remove(id: number): void {
  const index = todos.findIndex(t => t.id === id);
  if (index === -1) throw new NotFoundException(`Todo ${id} 不存在`);
  todos.splice(index, 1);
}
// findIndex 找索引,找不到返回 -1
// splice(index, 1) 从数组中删除一个元素
// 返回 void → Controller 中包装成 { message: 'success' }

⑤ update(id, patch):部分更新

update(id: number, patch: Partial<Todo>): Todo {
  const todo = todos.find(t => t.id === id);
  if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
  Object.assign(todo, patch);
  return todo;
}
// Partial<Todo> → Todo 的所有字段都变成可选
// Object.assign 把 patch 的字段合并到 todo
// 只更新传了的字段,没传的不变

5.4 Partial<T> 类型技巧

// Partial<T> 是 TypeScript 内置的工具类型
// 把接口的所有属性变成可选

interface Todo {
  id: number;
  title: string;
  complete: boolean;
}

type PartialTodo = Partial<Todo>;
// 等价于:
// {
//   id?: number;
//   title?: string;
//   complete?: boolean;
// }

// PATCH 请求时只传需要改的字段:
// PATCH /todos/1
// Body: { "complete": true }
// → patch = { complete: true }
// → Object.assign(todo, { complete: true })
// → 只改 complete,id 和 title 不变
Object.assign 合并原理:

  const todo = { id: 1, title: '学习', complete: false };
  const patch = { complete: true };

  Object.assign(todo, patch);
  → { id: 1, title: '学习', complete: true }
  //  patch 中有的字段覆盖 todo
  //  patch 中没有的字段保持不变

  注意:Object.assign 是浅拷贝
  如果 patch 中有嵌套对象,只是引用复制

5.5 NotFoundException:标准化错误处理

import { NotFoundException } from '@nestjs/common';

findOne(id: number): Todo {
  const todo = todos.find(t => t.id === id);
  if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
  return todo;
}
NestJS 内置错误类体系:

  NotFoundException      → 404 资源不存在
  BadRequestException   → 400 请求参数错误
  UnauthorizedException → 401 未认证
  ForbiddenException    → 403 无权限
  ConflictException     → 409 冲突(如重复创建)
  InternalServerErrorException → 500 服务器内部错误

  throw new NotFoundException(`Todo ${id} 不存在`)
  → NestJS 拦截异常,自动转成 HTTP 响应:
  {
    "statusCode": 404,
    "message": "Todo 5 不存在",
    "error": "Not Found"
  }

  对比原生 Node.js:
  → 需要手动 res.status(404).json({ ... })
  → NestJS 自动处理,开发者只需 throw
  → 这就是"标准化错误输出"
错误处理的演进:

  原生方式(手动处理):
    if (!todo) {
      res.status(404).json({ statusCode: 404, message: '不存在' });
      return;
    }

  NestJS 方式(异常驱动):
    if (!todo) throw new NotFoundException('不存在');
    → 框架自动转成 404 响应
    → 代码更简洁,关注业务逻辑而非响应格式

  传统 try/catch/finally:
    → 每个方法都要写 try/catch
    → 容易遗漏,线程挂掉
    → NestJS 用异常过滤器统一拦截
    → 开发者只需 throw,框架负责兜底

六、type import:TypeScript 的导入优化

6.1 区分类型导入和值导入

// Todos.controller.ts 中的导入
import { TodosService } from './Todos.service';       // 值导入
import type { Todo } from './Todos.service';            // 类型导入

为什么要分开?

// TodosService 是一个类(运行时存在)
import { TodosService } from './Todos.service';
// → 需要在运行时创建实例、依赖注入
// → 必须值导入

// Todo 是一个接口(编译时存在,运行时消失)
import type { Todo } from './Todos.service';
// → 只用于 TypeScript 类型标注
// → 编译后会被完全移除
// → 不产生运行时代码,减少打包体积
编译前:
  import { TodosService } from './Todos.service';
  import type { Todo } from './Todos.service';

  findAll(): Todo[] {
    return this.todosService.findAll();
  }

编译后(JavaScript):
  import { TodosService } from './Todos.service';
  // import type { Todo } → 完全消失!

  findAll() {
    return this.todosService.findAll();
  }
  // Todo[] 类型标注也消失了

6.2 type 导入的三种写法

// 写法一:独立 type import(推荐,语义最清晰)
import type { Todo } from './Todos.service';

// 写法二:内联 type 修饰符(TS 4.5+)
import { TodosService, type Todo } from './Todos.service';

// 写法三:不区分(编译器自动判断,但不推荐)
import { TodosService, Todo } from './Todos.service';
// → Todo 实际是 interface,编译器会自动移除
// → 但不够显式,可能影响 tree-shaking

七、完整请求-响应流程

7.1 端到端数据流

┌──────────────────────────────────────────────────────────────────┐
│                    完整请求-响应流程                               │
│                                                                  │
│  ① 浏览器发起 HTTP 请求                                           │
│     GET http://localhost:3000/todos/1                            │
│                                                                  │
│  ② NestJS 路由匹配                                               │
│     → @Controller('todos') 前缀匹配 /todos                       │
│     → @Get(':id') 方法匹配 /todos/1                              │
│     → 提取路径参数 id = '1'                                      │
│                                                                  │
│  ③ 参数装饰器执行                                                 │
│     @Param('id') id: string → id = '1'                          │
│     → URL 参数永远是 string 类型                                  │
│                                                                  │
│  ④ Controller 方法执行                                           │
│     findOne('1')                                                 │
│     → Number('1') → 1                                            │
│     → this.todosService.findOne(1)                               │
│                                                                  │
│  ⑤ Service 业务逻辑                                              │
│     todos.find(t => t.id === 1)                                  │
│     → 找到 { id: 1, title: '学习 NestJS', complete: false }     │
│     → 返回 todo 对象                                             │
│                                                                  │
│     如果找不到:                                                  │
│     → throw new NotFoundException('Todo 1 不存在')              │
│     → NestJS 异常过滤器拦截                                      │
│     → 自动返回 404 响应                                          │
│                                                                  │
│  ⑥ Controller 返回响应                                           │
│     → return todo                                                │
│     → NestJS 自动序列化为 JSON                                    │
│     → HTTP 200 + JSON body                                      │
│                                                                  │
│  ⑦ 浏览器收到响应                                                 │
│     200 OK                                                       │
│     { "id": 1, "title": "学习 NestJS", "complete": false }     │
└──────────────────────────────────────────────────────────────────┘

7.2 五个接口的请求与响应

① 获取所有
  GET /todos
  → 200 OK
  → [
      { "id": 1, "title": "学习 NestJS", "complete": false },
      { "id": 2, "title": "学习 CRUD", "complete": true }
    ]

② 获取单个
  GET /todos/1
  → 200 OK
  → { "id": 1, "title": "学习 NestJS", "complete": false }

  GET /todos/999
  → 404 Not Found
  → { "statusCode": 404, "message": "Todo 999 不存在", "error": "Not Found" }

③ 创建
  POST /todos
  Body: { "title": "学习装饰器" }
  → 201 Created
  → { "id": 3, "title": "学习装饰器", "complete": false }

④ 删除
  DELETE /todos/1
  → 200 OK
  → { "message": "success" }

  DELETE /todos/999
  → 404 Not Found
  → { "statusCode": 404, "message": "Todo 999 不存在", "error": "Not Found" }

⑤ 部分更新
  PATCH /todos/1
  Body: { "complete": true }
  → 200 OK
  → { "id": 1, "title": "学习 NestJS", "complete": true }

八、NestJS 装饰器全景图

8.1 七大核心装饰器

┌──────────────────────────────────────────────────────────────────┐
│                 NestJS 七大核心装饰器                              │
│                                                                  │
│  类装饰器(修饰整个类)                                           │
│  ├── @Controller('todos')  → 设置路由前缀,标记为控制器            │
│  ├── @Module({ ... })     → 组织模块结构                         │
│  └── @Injectable()        → 声明服务可被依赖注入                  │
│                                                                  │
│  方法装饰器(修饰类的方法)                                       │
│  ├── @Get()               → GET 路由                             │
│  ├── @Post()              → POST 路由                            │
│  ├── @Patch(':id')        → PATCH 路由                           │
│  └── @Delete(':id')       → DELETE 路由                          │
│                                                                  │
│  参数装饰器(修饰方法参数)                                       │
│  ├── @Param('id')         → 从 URL 路径提取参数                 │
│  └── @Body() / @Body('title') → 从请求体提取数据                │
└──────────────────────────────────────────────────────────────────┘

8.2 装饰器在各层的分布

Controller 层使用的装饰器:
  @Controller('todos')        → 类装饰器:路由前缀
  @Get() / @Post() / ...      → 方法装饰器:HTTP 路由
  @Param('id')                → 参数装饰器:路径参数
  @Body() / @Body('title')   → 参数装饰器:请求体
  constructor(private readonly todosService: TodosService)  → 依赖注入

Service 层使用的装饰器:
  @Injectable()               → 类装饰器:可注入

Module 层使用的装饰器:
  @Module({ imports, controllers, providers })  → 类装饰器:模块组装

九、NestJS 分层架构总结

9.1 三层职责边界

┌──────────────────────────────────────────────────────────┐
│                  NestJS 三层架构                          │
│                                                          │
│  ┌──────────────────────────────────────┐               │
│  │  Module 层(组装层)                   │               │
│  │  ├── @Module 装饰器                   │               │
│  │  ├── imports: 子模块依赖              │               │
│  │  ├── controllers: 注册控制器          │               │
│  │  └── providers: 注册服务              │               │
│  │  职责:组装,不写业务逻辑               │               │
│  └──────────────┬───────────────────────┘               │
│                 │                                        │
│  ┌──────────────▼───────────────────────┐               │
│  │  Controller 层(控制层)               │               │
│  │  ├── @Controller + @Get/@Post/...     │               │
│  │  ├── @Param + @Body 参数提取          │               │
│  │  ├── 参数校验                         │               │
│  │  ├── 调用 Service                     │               │
│  │  └── return 响应                      │               │
│  │  职责:路由 + 参数校验,不写业务逻辑    │               │
│  └──────────────┬───────────────────────┘               │
│                 │                                        │
│  ┌──────────────▼───────────────────────┐               │
│  │  Service 层(业务层)                  │               │
│  │  ├── @Injectable 可注入               │               │
│  │  ├── 业务逻辑处理                      │               │
│  │  ├── 数据 CRUD                        │               │
│  │  ├── 错误处理(throw NotFoundException)│              │
│  │  └── return 数据                      │               │
│  │  职责:所有业务逻辑都在这里             │               │
│  └──────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────┘

9.2 各层"不做"什么

Module 不做:
  ❌ 不写业务逻辑
  ❌ 不处理 HTTP 请求
  ❌ 不操作数据库

Controller 不做:
  ❌ 不直接操作数据库
  ❌ 不写复杂业务逻辑
  ❌ 不做数据处理与转换

Service 不做:
  ❌ 不处理 HTTP 路由(不关心 URL 是什么)
  ❌ 不解析请求参数(参数已被 Controller 提取)
  ❌ 不格式化 HTTP 响应(返回纯数据,NestJS 自动序列化)

十、总结

10.1 知识体系图

NestJS CRUD 接口工程
│
├── 模块化开发流程
│   ├── AppModule imports 植入子模块
│   ├── 业务模块 = Module + Controller + Service
│   └── @Module({ controllers, pro