NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理

NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理

本文基于一个真实可运行的 NestJS hello 项目(NestJS 11.0.1 + TypeScript 5.7.3),用 7 个源文件 + 5 个 CRUD 接口,把 NestJS 大厂面试高频考点一次性讲透:MVC 三层落地、三层装饰器系统、依赖注入链路、内置异常类实战。文中代码来自真实项目,运行未验证,仅用于讲解结构。

写在前面:为什么是 hello 项目

很多 NestJS 教程用「玩具例子」讲概念------单独贴一段 @Get() 你以为懂了,回到真实项目还是懵。问题不在你,在例子。本文换个思路:拿一个能跑起来的 hello 项目,7 个文件全部摆出来,5 个 CRUD 接口逐个拆,让你看到 MVC 不是教科书三层等分,而是「Controller 接 HTTP + Service 写业务 + Module 组装」的工程分工。

理解这 7 个文件的协作,就能回答 NestJS 大厂面试的 80% 问题。剩下 20% 是 TypeORM、微服务、守卫拦截器这些进阶话题,不在本文范围。

文章目录

  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」

一、7 个文件的全景图:谁干什么

hello 项目的目录结构如下(已省略配置文件):

text 复制代码
hello/
├── src/
│   ├── main.ts                  # 入口:工厂启动
│   ├── app.module.ts            # 根模块:三件套组装
│   ├── app.controller.ts        # 根控制器:处理 GET /
│   ├── app.service.ts           # 根服务:返回 Hello World
│   └── todos/
│       ├── Todos.module.ts      # 业务模块:组装 todos 三件套
│       ├── Todos.controller.ts  # 业务控制器:5 个 CRUD 接口
│       └── Todos.service.ts     # 业务服务:5 个 CRUD 方法
└── package.json

对应到 MVC 三层:

文件 MVC 角色 职责
main.ts 启动器 工厂创建应用 + 监听端口
*.module.ts 组装层 注册 Controller + Service,划分业务边界
*.controller.ts Controller 接 HTTP 请求、提参、调 Service、返响应
*.service.ts Service + Model 业务逻辑 + 数据存储(hello 用数组模拟)
AppService / TodosService Model 抽象 生产环境换成 TypeORM Repository

核心判断 :NestJS 的 V(View)不是 HTML 模板,而是 NestJS 自动把返回值 JSON.stringify 后写进 HTTP body;M(Model)在 hello 项目用模块级 let todos: TodoItem[] 数组模拟,生产换成 TypeORM。

二、main.ts:工厂模式 + 启动入口

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);
}
bootstrap();

4 个关键点

  1. NestFactory.create(AppModule) 是工厂模式落地------你不 new NestApplication(),而是把「怎么造应用」的细节交给工厂,调用方只关心「我要一个能跑的应用」。
  2. AppModule 是入口参数,意味着整个应用的依赖图都从这个根模块展开。
  3. process.env.PORT ?? 3000 用空值合并运算符兜底端口,云平台注入 PORT 环境变量时自动让位。
  4. bootstrap() 是 async 函数,调用时没 await------这是 Node 启动脚本的惯例,主线程跑完启动即可交出控制权。

面试角度:NestFactory.create 为什么是工厂模式?答:调用方不直接 new,由工厂统一创建并初始化依赖图,符合「开闭原则」,未来换底层实现(如 Fastify)不影响调用方。

三、AppModule:三件套组装根模块

typescript 复制代码
// src/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],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@Module 装饰器接收三个关键字段:

  • imports :引入其他模块,决定「哪些业务能被这个模块看到」。AppModule 导入 TodosModule,相当于把 todos 业务的依赖图挂到根上。
  • controllers :注册本模块的控制器类,NestJS 会扫描其中的 @Get/@Post 等装饰器建立路由表。
  • providers:注册本模块的 Service / Repository,被注册的类进入 IoC 容器,可被自动注入。

模块化好处(来自 main.ts 注释):如果一个文件几千行代码不行,所以要模块化;大型项目按业务划分 Module,NestJS 还会按需加载做性能优化。

四、三层装饰器系统:NestJS 的灵魂

hello 项目用到的装饰器分三类,这是大厂面试必问点

类别 装饰器 作用位置 干什么
类装饰器 @Module / @Controller / @Injectable 类声明前 给类附加元数据,标记「我是模块/控制器/可注入服务」
方法装饰器 @Get / @Post / @Put / @Delete 类方法前 把方法绑到 HTTP 路由
参数装饰器 @Param / @Body / @Query 方法参数前 从请求对象提取数据注入参数

readme.md 评价:「装饰器模式用到极致」。装饰器本质是「在不修改原有对象的前提下,动态添加额外功能」,用 @ 表示------可以理解成「给类贴一张说明书,NestJS 看到说明书就知道怎么处理它」。

五、AppController:依赖注入语法糖

typescript 复制代码
// src/app.controller.ts(运行未验证)
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

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

重点拆解 constructor 那一行------这是 TS 参数属性语法糖:

typescript 复制代码
constructor(private readonly appService: AppService) {}

等价于这三步:

typescript 复制代码
class AppController {
  private readonly appService: AppService;
  constructor(appService: AppService) {
    this.appService = appService;
  }
}

NestJS 看到这个构造参数类型是 AppService,去 IoC 容器找已注册的单例,自动传进来------程序员不用 new、不用手动传参。这就是依赖注入。

依赖注入链路 5 步(收藏资产)

text 复制代码
1. @Module providers:[AppService]    → 注册到 IoC 容器
2. @Injectable() 装饰 AppService 类  → 标记「我可被注入」
3. constructor 参数声明依赖           → 告诉容器「我需要 AppService」
4. 容器 new Controller 时自动传单例   → 解决依赖
5. this.appService.method() 调用    → 业务执行

面试金句 :依赖注入解决的是「组件之间耦合」的问题------Controller 不再 new Service,而是声明「我要什么」,由容器负责供给。好处是 Controller 和 Service 解耦,Service 可单测可替换。

六、TodosController:5 个 CRUD 接口实战

这是本文最核心的代码------一个文件覆盖 5 种 HTTP 方法 + 3 种参数装饰器。

typescript 复制代码
// src/todos/Todos.controller.ts(运行未验证)
import { Controller, Get, Post, Param, Body, Delete, Put } from '@nestjs/common';
import { TodosService } from './Todos.service';
import { type TodoItem } from './Todos.service';

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

  @Get()
  findAll(): TodoItem[] {
    return this.todosService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): TodoItem {
    return this.todosService.findOne(Number(id));
  }

  @Post()
  create(@Body('title') title: string): TodoItem {
    return this.todosService.create(title);
  }

  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: `删除 todoItem ${id}` };
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() patch: Partial<TodoItem>): TodoItem {
    return this.todosService.update(Number(id), patch);
  }
}

路由匹配对照表(收藏资产)

HTTP 方法 装饰器组合 完整路径 调用方法
GET @Controller('todos') + @Get() GET /todos findAll
GET @Controller('todos') + @Get(':id') GET /todos/:id findOne
POST @Controller('todos') + @Post() POST /todos create
DELETE @Controller('todos') + @Delete(':id') DELETE /todos/:id remove
PUT @Controller('todos') + @Put(':id') PUT /todos/:id update

关键细节

  1. 路由拼接规则 :完整路径 = @Controller(prefix) + @Method(subpath)@Controller('todos') 是类级前缀,所有方法共享。
  2. RESTful 标准 (注释原话:restful 暴露资源的统一标准):同一资源用 HTTP 方法区分操作,URL 是名词不是动词。
  3. URL 参数永远是字符串@Param('id') 拿到的是 string 类型,必须 Number(id)number。如果用户访问 /todos/abc,会得到 NaNArray.find 返回 undefined,最终抛 NotFoundException → HTTP 404。

@Body() vs @Body('field') 对比

用法 提取内容 类型 配合 TS
@Body('title') title: string 只取 body 中的 title 字段 单字段 直接指定类型
@Body() patch: Partial<TodoItem> 注入整个请求体对象 整个 body 配合 Partial<T>

Partial<TodoItem>TodoItem 所有属性变可选,方便 PATCH 语义(只更新传入字段)。但要注意:这会让 id 也可被改 ,存在 OWASP Mass Assignment 漏洞。生产建议用 Pick<TodoItem, 'title' | 'complete'> 限制可改字段。

import { type TodoItem } 是什么意思

typescript 复制代码
import { type TodoItem } from './Todos.service';

这是 TS 的 import type 语法。原因:当 tsconfig 同时开 isolatedModulesemitDecoratorMetadata 时,被装饰器参数引用的 interface 必须用 import type,否则报 TS1272: A type referenced in a decorated signature must be imported with 'import type'

七、TodosService:业务逻辑 + 错误处理

typescript 复制代码
// src/todos/Todos.service.ts(运行未验证)
import { Injectable, NotFoundException } from '@nestjs/common';

export interface TodoItem {
  id: number;
  title: string;
  complete: boolean;
}

let todos: TodoItem[] = [
  { id: 1, title: '学习 NestJS', complete: false },
  { id: 2, title: '学习 CRUD', complete: true },
];
let nextId = 3;

@Injectable()
export class TodosService {
  findAll(): TodoItem[] {
    return todos;
  }

  findOne(id: number): TodoItem {
    const todoItem = todos.find(t => t.id === id);
    if (!todoItem) throw new NotFoundException(`TodoItem ${id} 不存在`);
    return todoItem;
  }

  create(title: string): TodoItem {
    const todoItem: TodoItem = { id: nextId++, title, complete: false };
    todos.push(todoItem);
    return todoItem;
  }

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

  update(id: number, patch: Partial<TodoItem>): TodoItem {
    const todoItem = this.findOne(id);
    Object.assign(todoItem, patch);
    return todoItem;
  }
}

重点 1:模拟数据库

let todos: TodoItem[]let nextId = 3 是模块级变量,重启会丢失。生产环境换成 TypeORM 的 Repository,写法变化不大------这是 NestJS 分层的好处,存储层换底层不动业务逻辑。

重点 2:@Injectable() 干什么

注释原话:「把一个普通类变成 Nest 容器可管理的服务,从而能被自动实例化和注入到任何需要它的地方」。换句话说,加了 @Injectable(),这个类的实例就能被 IoC 容器统一管理,默认是单例

重点 3:NotFoundException 错误处理(大厂面试高频)

findOne 找不到数据时抛 throw new NotFoundException(...),NestJS 自动转成 HTTP 404 响应,body 类似:

json 复制代码
{
  "statusCode": 404,
  "message": "TodoItem 999 不存在",
  "error": "Not Found"
}
throw new Error vs throw new NotFoundException 对比
写法 HTTP 状态码 语义 推荐度
throw new Error('出错了') 500 服务器内部错误 ❌ 语义错误
throw new NotFoundException('出错了') 404 资源不存在 ✅ 语义正确

面试金句:查不到数据是「客户端的锅」(请求了不存在的资源),应该返回 404;如果返回 500 等于把锅甩给服务器,会让前端误判是后端崩了。

重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)

异常类 状态码 使用场景
BadRequestException 400 参数校验失败
UnauthorizedException 401 未登录
ForbiddenException 403 登录但无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突(如重复注册)
InternalServerErrorException 500 真正的服务器错误

所有类都继承自 HttpException,NestJS 拦截到这些异常会自动序列化成标准 JSON 响应。

这就是 readme.md 提到的「NestJS 提供各种错误类,标准化错误输出------status code 状态码 + message 消息」。面试题「请说下你是如何处理后端报错的」答案就在这里:用语义化异常类,让 NestJS 框架统一兜底。

八、完整请求链路图:从浏览器到 Service

POST /todos 为例(来自 Todos.controller.ts 文件底部 ASCII 注释):

text 复制代码
前端  POST /todos   body: {"title":"学习 React"}
  ↓
NestJS 路由匹配:@Controller('todos') + @Post()
  ↓
进入 create,@Body('title') 提取出 title = "学习 React"
  ↓
this.todosService.create("学习 React")
  ↓
service: 生成新对象 {id:3, title:"学习 React", complete:false}
  ↓
push 进 todos 数组
  ↓
NestJS 把返回值 JSON.stringify 后写入 HTTP body
  ↓
浏览器收到 201 + {"id":3,"title":"学习 React","complete":false}

根路径 GET / 的链路(来自 readme.md):

text 复制代码
浏览器 GET http://localhost:3000
  ↓
main.ts app.listen(3000) 接收
  ↓
路由匹配 GET /
  ↓
找到 AppController(由 AppModule 注册)
  ↓
AppController 的 @Get() 装饰器匹配
  ↓
调用 getHello()
  ↓
调用 this.appService.getHello()
  ↓
AppService.getHello() 返回 'Hello World!'
  ↓
浏览器显示 Hello World!

九、大厂面试高频问题清单

整理 8 个高频考点,对应文中事实编号:

  1. NestJS MVC 三层在 hello 项目怎么落地 ------M 是数组模拟、V 是 NestJS 自动 JSON、C 是 @Controller + HTTP 方法装饰器。
  2. NestJS 请求处理流程------浏览器 → main.ts → 路由匹配 → Controller → Service → JSON 响应。
  3. 三类装饰器分别干什么------类装饰器附加元数据、方法装饰器绑路由、参数装饰器提参。
  4. 依赖注入链路 5 步 ------providers 注册 → @Injectable 标记 → constructor 声明 → 容器传单例 → 调用方法。
  5. @Body() vs @Body('field') 区别 ------整体注入 vs 单字段提取,前者配合 Partial<T>,后者适合只要单字段。
  6. URL 参数为什么是字符串 ------HTTP 协议层就是文本,/todos/abc 拿到的是 'abc',需要 Number() 转换,转换失败得 NaN,最终触发 404。
  7. NestJS 内置错误类解决什么问题------标准化错误输出(status + message),让前端按状态码分支处理。
  8. throw new Error vs throw new NotFoundException------500 vs 404,语义正确性决定接口设计质量。

十、自检清单:你真的看懂 hello 项目了吗

读完本文,对照 8 个自检点,能答 6 个以上才算真懂:

  • NestFactory.create 为什么是工厂模式?调用方不直接 new,由工厂统一创建。
  • @Moduleimports / controllers / providers 分别装什么?
  • constructor(private readonly appService: AppService) 这一行展开是几步?3 步。
  • @Controller('todos') + @Get(':id') 匹配的完整路径是什么?GET /todos/:id
  • @Param('id') 拿到的是什么类型?string,必须 Number() 转。
  • @Body('title')@Body() 区别是什么?单字段 vs 整体注入。
  • NotFoundException 对应 HTTP 几?404。
  • Partial<TodoItem> 有什么安全风险?让 id 也可被改,存在 Mass Assignment 漏洞。

收尾:从「背文档」到「看懂项目」

NestJS 的学习陷阱在于:装饰器语法看起来简单,但理解「为什么这么设计」比记住「怎么写」重要得多。hello 项目 7 个文件 + 5 个 CRUD 接口把这个「为什么」完整跑通------main.ts 工厂启动、AppModule 三件套组装、TodosController 5 种 HTTP 方法装饰器、TodosService 5 个 CRUD 方法 + NotFoundException 容错。每个概念都能在真实代码里找到对应位置,比单独看装饰器、依赖注入、异常处理的孤岛文档要扎实得多。

下一步建议:把 hello 项目 clone 下来跑一遍,再用 Postman 打 5 个接口,对照本文的链路图看 console.log 输出。当你能凭记忆画出「浏览器 → Controller → Service → JSON 响应」的完整链路,NestJS 大厂面试的 MVC + 装饰器 + DI + 错误处理这四大考点就稳了。

进阶方向(不在本文范围):TypeORM 替换数组存储、自定义 ExceptionFilter 统一错误格式、Guard 守卫做鉴权、Interceptor 拦截器做日志。这些是 hello 项目跑通后的下一站。

标签:NestJS,MVC 架构,依赖注入,装饰器,后端面试

NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理

本文基于一个真实可运行的 NestJS hello 项目(NestJS 11.0.1 + TypeScript 5.7.3),用 7 个源文件 + 5 个 CRUD 接口,把 NestJS 大厂面试高频考点一次性讲透:MVC 三层落地、三层装饰器系统、依赖注入链路、内置异常类实战。文中代码来自真实项目,运行未验证,仅用于讲解结构。

写在前面:为什么是 hello 项目

很多 NestJS 教程用「玩具例子」讲概念------单独贴一段 @Get() 你以为懂了,回到真实项目还是懵。问题不在你,在例子。本文换个思路:拿一个能跑起来的 hello 项目,7 个文件全部摆出来,5 个 CRUD 接口逐个拆,让你看到 MVC 不是教科书三层等分,而是「Controller 接 HTTP + Service 写业务 + Module 组装」的工程分工。

理解这 7 个文件的协作,就能回答 NestJS 大厂面试的 80% 问题。剩下 20% 是 TypeORM、微服务、守卫拦截器这些进阶话题,不在本文范围。

文章目录

  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」

一、7 个文件的全景图:谁干什么

hello 项目的目录结构如下(已省略配置文件):

text 复制代码
hello/
├── src/
│   ├── main.ts                  # 入口:工厂启动
│   ├── app.module.ts            # 根模块:三件套组装
│   ├── app.controller.ts        # 根控制器:处理 GET /
│   ├── app.service.ts           # 根服务:返回 Hello World
│   └── todos/
│       ├── Todos.module.ts      # 业务模块:组装 todos 三件套
│       ├── Todos.controller.ts  # 业务控制器:5 个 CRUD 接口
│       └── Todos.service.ts     # 业务服务:5 个 CRUD 方法
└── package.json

对应到 MVC 三层:

文件 MVC 角色 职责
main.ts 启动器 工厂创建应用 + 监听端口
*.module.ts 组装层 注册 Controller + Service,划分业务边界
*.controller.ts Controller 接 HTTP 请求、提参、调 Service、返响应
*.service.ts Service + Model 业务逻辑 + 数据存储(hello 用数组模拟)
AppService / TodosService Model 抽象 生产环境换成 TypeORM Repository

核心判断 :NestJS 的 V(View)不是 HTML 模板,而是 NestJS 自动把返回值 JSON.stringify 后写进 HTTP body;M(Model)在 hello 项目用模块级 let todos: TodoItem[] 数组模拟,生产换成 TypeORM。

二、main.ts:工厂模式 + 启动入口

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);
}
bootstrap();

4 个关键点

  1. NestFactory.create(AppModule) 是工厂模式落地------你不 new NestApplication(),而是把「怎么造应用」的细节交给工厂,调用方只关心「我要一个能跑的应用」。
  2. AppModule 是入口参数,意味着整个应用的依赖图都从这个根模块展开。
  3. process.env.PORT ?? 3000 用空值合并运算符兜底端口,云平台注入 PORT 环境变量时自动让位。
  4. bootstrap() 是 async 函数,调用时没 await------这是 Node 启动脚本的惯例,主线程跑完启动即可交出控制权。

面试角度:NestFactory.create 为什么是工厂模式?答:调用方不直接 new,由工厂统一创建并初始化依赖图,符合「开闭原则」,未来换底层实现(如 Fastify)不影响调用方。

三、AppModule:三件套组装根模块

typescript 复制代码
// src/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],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@Module 装饰器接收三个关键字段:

  • imports :引入其他模块,决定「哪些业务能被这个模块看到」。AppModule 导入 TodosModule,相当于把 todos 业务的依赖图挂到根上。
  • controllers :注册本模块的控制器类,NestJS 会扫描其中的 @Get/@Post 等装饰器建立路由表。
  • providers:注册本模块的 Service / Repository,被注册的类进入 IoC 容器,可被自动注入。

模块化好处(来自 main.ts 注释):如果一个文件几千行代码不行,所以要模块化;大型项目按业务划分 Module,NestJS 还会按需加载做性能优化。

四、三层装饰器系统:NestJS 的灵魂

hello 项目用到的装饰器分三类,这是大厂面试必问点

类别 装饰器 作用位置 干什么
类装饰器 @Module / @Controller / @Injectable 类声明前 给类附加元数据,标记「我是模块/控制器/可注入服务」
方法装饰器 @Get / @Post / @Put / @Delete 类方法前 把方法绑到 HTTP 路由
参数装饰器 @Param / @Body / @Query 方法参数前 从请求对象提取数据注入参数

readme.md 评价:「装饰器模式用到极致」。装饰器本质是「在不修改原有对象的前提下,动态添加额外功能」,用 @ 表示------可以理解成「给类贴一张说明书,NestJS 看到说明书就知道怎么处理它」。

五、AppController:依赖注入语法糖

typescript 复制代码
// src/app.controller.ts(运行未验证)
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

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

重点拆解 constructor 那一行------这是 TS 参数属性语法糖:

typescript 复制代码
constructor(private readonly appService: AppService) {}

等价于这三步:

typescript 复制代码
class AppController {
  private readonly appService: AppService;
  constructor(appService: AppService) {
    this.appService = appService;
  }
}

NestJS 看到这个构造参数类型是 AppService,去 IoC 容器找已注册的单例,自动传进来------程序员不用 new、不用手动传参。这就是依赖注入。

依赖注入链路 5 步(收藏资产)

text 复制代码
1. @Module providers:[AppService]    → 注册到 IoC 容器
2. @Injectable() 装饰 AppService 类  → 标记「我可被注入」
3. constructor 参数声明依赖           → 告诉容器「我需要 AppService」
4. 容器 new Controller 时自动传单例   → 解决依赖
5. this.appService.method() 调用    → 业务执行

面试金句 :依赖注入解决的是「组件之间耦合」的问题------Controller 不再 new Service,而是声明「我要什么」,由容器负责供给。好处是 Controller 和 Service 解耦,Service 可单测可替换。

六、TodosController:5 个 CRUD 接口实战

这是本文最核心的代码------一个文件覆盖 5 种 HTTP 方法 + 3 种参数装饰器。

typescript 复制代码
// src/todos/Todos.controller.ts(运行未验证)
import { Controller, Get, Post, Param, Body, Delete, Put } from '@nestjs/common';
import { TodosService } from './Todos.service';
import { type TodoItem } from './Todos.service';

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

  @Get()
  findAll(): TodoItem[] {
    return this.todosService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): TodoItem {
    return this.todosService.findOne(Number(id));
  }

  @Post()
  create(@Body('title') title: string): TodoItem {
    return this.todosService.create(title);
  }

  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: `删除 todoItem ${id}` };
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() patch: Partial<TodoItem>): TodoItem {
    return this.todosService.update(Number(id), patch);
  }
}

路由匹配对照表(收藏资产)

HTTP 方法 装饰器组合 完整路径 调用方法
GET @Controller('todos') + @Get() GET /todos findAll
GET @Controller('todos') + @Get(':id') GET /todos/:id findOne
POST @Controller('todos') + @Post() POST /todos create
DELETE @Controller('todos') + @Delete(':id') DELETE /todos/:id remove
PUT @Controller('todos') + @Put(':id') PUT /todos/:id update

关键细节

  1. 路由拼接规则 :完整路径 = @Controller(prefix) + @Method(subpath)@Controller('todos') 是类级前缀,所有方法共享。
  2. RESTful 标准 (注释原话:restful 暴露资源的统一标准):同一资源用 HTTP 方法区分操作,URL 是名词不是动词。
  3. URL 参数永远是字符串@Param('id') 拿到的是 string 类型,必须 Number(id)number。如果用户访问 /todos/abc,会得到 NaNArray.find 返回 undefined,最终抛 NotFoundException → HTTP 404。

@Body() vs @Body('field') 对比

用法 提取内容 类型 配合 TS
@Body('title') title: string 只取 body 中的 title 字段 单字段 直接指定类型
@Body() patch: Partial<TodoItem> 注入整个请求体对象 整个 body 配合 Partial<T>

Partial<TodoItem>TodoItem 所有属性变可选,方便 PATCH 语义(只更新传入字段)。但要注意:这会让 id 也可被改 ,存在 OWASP Mass Assignment 漏洞。生产建议用 Pick<TodoItem, 'title' | 'complete'> 限制可改字段。

import { type TodoItem } 是什么意思

typescript 复制代码
import { type TodoItem } from './Todos.service';

这是 TS 的 import type 语法。原因:当 tsconfig 同时开 isolatedModulesemitDecoratorMetadata 时,被装饰器参数引用的 interface 必须用 import type,否则报 TS1272: A type referenced in a decorated signature must be imported with 'import type'

七、TodosService:业务逻辑 + 错误处理

typescript 复制代码
// src/todos/Todos.service.ts(运行未验证)
import { Injectable, NotFoundException } from '@nestjs/common';

export interface TodoItem {
  id: number;
  title: string;
  complete: boolean;
}

let todos: TodoItem[] = [
  { id: 1, title: '学习 NestJS', complete: false },
  { id: 2, title: '学习 CRUD', complete: true },
];
let nextId = 3;

@Injectable()
export class TodosService {
  findAll(): TodoItem[] {
    return todos;
  }

  findOne(id: number): TodoItem {
    const todoItem = todos.find(t => t.id === id);
    if (!todoItem) throw new NotFoundException(`TodoItem ${id} 不存在`);
    return todoItem;
  }

  create(title: string): TodoItem {
    const todoItem: TodoItem = { id: nextId++, title, complete: false };
    todos.push(todoItem);
    return todoItem;
  }

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

  update(id: number, patch: Partial<TodoItem>): TodoItem {
    const todoItem = this.findOne(id);
    Object.assign(todoItem, patch);
    return todoItem;
  }
}

重点 1:模拟数据库

let todos: TodoItem[]let nextId = 3 是模块级变量,重启会丢失。生产环境换成 TypeORM 的 Repository,写法变化不大------这是 NestJS 分层的好处,存储层换底层不动业务逻辑。

重点 2:@Injectable() 干什么

注释原话:「把一个普通类变成 Nest 容器可管理的服务,从而能被自动实例化和注入到任何需要它的地方」。换句话说,加了 @Injectable(),这个类的实例就能被 IoC 容器统一管理,默认是单例

重点 3:NotFoundException 错误处理(大厂面试高频)

findOne 找不到数据时抛 throw new NotFoundException(...),NestJS 自动转成 HTTP 404 响应,body 类似:

json 复制代码
{
  "statusCode": 404,
  "message": "TodoItem 999 不存在",
  "error": "Not Found"
}
throw new Error vs throw new NotFoundException 对比
写法 HTTP 状态码 语义 推荐度
throw new Error('出错了') 500 服务器内部错误 ❌ 语义错误
throw new NotFoundException('出错了') 404 资源不存在 ✅ 语义正确

面试金句:查不到数据是「客户端的锅」(请求了不存在的资源),应该返回 404;如果返回 500 等于把锅甩给服务器,会让前端误判是后端崩了。

重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)

异常类 状态码 使用场景
BadRequestException 400 参数校验失败
UnauthorizedException 401 未登录
ForbiddenException 403 登录但无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突(如重复注册)
InternalServerErrorException 500 真正的服务器错误

所有类都继承自 HttpException,NestJS 拦截到这些异常会自动序列化成标准 JSON 响应。

这就是 readme.md 提到的「NestJS 提供各种错误类,标准化错误输出------status code 状态码 + message 消息」。面试题「请说下你是如何处理后端报错的」答案就在这里:用语义化异常类,让 NestJS 框架统一兜底。

八、完整请求链路图:从浏览器到 Service

POST /todos 为例(来自 Todos.controller.ts 文件底部 ASCII 注释):

text 复制代码
前端  POST /todos   body: {"title":"学习 React"}
  ↓
NestJS 路由匹配:@Controller('todos') + @Post()
  ↓
进入 create,@Body('title') 提取出 title = "学习 React"
  ↓
this.todosService.create("学习 React")
  ↓
service: 生成新对象 {id:3, title:"学习 React", complete:false}
  ↓
push 进 todos 数组
  ↓
NestJS 把返回值 JSON.stringify 后写入 HTTP body
  ↓
浏览器收到 201 + {"id":3,"title":"学习 React","complete":false}

根路径 GET / 的链路(来自 readme.md):

text 复制代码
浏览器 GET http://localhost:3000
  ↓
main.ts app.listen(3000) 接收
  ↓
路由匹配 GET /
  ↓
找到 AppController(由 AppModule 注册)
  ↓
AppController 的 @Get() 装饰器匹配
  ↓
调用 getHello()
  ↓
调用 this.appService.getHello()
  ↓
AppService.getHello() 返回 'Hello World!'
  ↓
浏览器显示 Hello World!

九、大厂面试高频问题清单

整理 8 个高频考点,对应文中事实编号:

  1. NestJS MVC 三层在 hello 项目怎么落地 ------M 是数组模拟、V 是 NestJS 自动 JSON、C 是 @Controller + HTTP 方法装饰器。
  2. NestJS 请求处理流程------浏览器 → main.ts → 路由匹配 → Controller → Service → JSON 响应。
  3. 三类装饰器分别干什么------类装饰器附加元数据、方法装饰器绑路由、参数装饰器提参。
  4. 依赖注入链路 5 步 ------providers 注册 → @Injectable 标记 → constructor 声明 → 容器传单例 → 调用方法。
  5. @Body() vs @Body('field') 区别 ------整体注入 vs 单字段提取,前者配合 Partial<T>,后者适合只要单字段。
  6. URL 参数为什么是字符串 ------HTTP 协议层就是文本,/todos/abc 拿到的是 'abc',需要 Number() 转换,转换失败得 NaN,最终触发 404。
  7. NestJS 内置错误类解决什么问题------标准化错误输出(status + message),让前端按状态码分支处理。
  8. throw new Error vs throw new NotFoundException------500 vs 404,语义正确性决定接口设计质量。

十、自检清单:你真的看懂 hello 项目了吗

读完本文,对照 8 个自检点,能答 6 个以上才算真懂:

  • NestFactory.create 为什么是工厂模式?调用方不直接 new,由工厂统一创建。
  • @Moduleimports / controllers / providers 分别装什么?
  • constructor(private readonly appService: AppService) 这一行展开是几步?3 步。
  • @Controller('todos') + @Get(':id') 匹配的完整路径是什么?GET /todos/:id
  • @Param('id') 拿到的是什么类型?string,必须 Number() 转。
  • @Body('title')@Body() 区别是什么?单字段 vs 整体注入。
  • NotFoundException 对应 HTTP 几?404。
  • Partial<TodoItem> 有什么安全风险?让 id 也可被改,存在 Mass Assignment 漏洞。

收尾:从「背文档」到「看懂项目」

NestJS 的学习陷阱在于:装饰器语法看起来简单,但理解「为什么这么设计」比记住「怎么写」重要得多。hello 项目 7 个文件 + 5 个 CRUD 接口把这个「为什么」完整跑通------main.ts 工厂启动、AppModule 三件套组装、TodosController 5 种 HTTP 方法装饰器、TodosService 5 个 CRUD 方法 + NotFoundException 容错。每个概念都能在真实代码里找到对应位置,比单独看装饰器、依赖注入、异常处理的孤岛文档要扎实得多。

下一步建议:把 hello 项目 clone 下来跑一遍,再用 Postman 打 5 个接口,对照本文的链路图看 console.log 输出。当你能凭记忆画出「浏览器 → Controller → Service → JSON 响应」的完整链路,NestJS 大厂面试的 MVC + 装饰器 + DI + 错误处理这四大考点就稳了。

进阶方向(不在本文范围):TypeORM 替换数组存储、自定义 ExceptionFilter 统一错误格式、Guard 守卫做鉴权、Interceptor 拦截器做日志。这些是 hello 项目跑通后的下一站。

标签:NestJS,MVC 架构,依赖注入,装饰器,后端面试

NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理

本文基于一个真实可运行的 NestJS hello 项目(NestJS 11.0.1 + TypeScript 5.7.3),用 7 个源文件 + 5 个 CRUD 接口,把 NestJS 大厂面试高频考点一次性讲透:MVC 三层落地、三层装饰器系统、依赖注入链路、内置异常类实战。文中代码来自真实项目,运行未验证,仅用于讲解结构。

写在前面:为什么是 hello 项目

很多 NestJS 教程用「玩具例子」讲概念------单独贴一段 @Get() 你以为懂了,回到真实项目还是懵。问题不在你,在例子。本文换个思路:拿一个能跑起来的 hello 项目,7 个文件全部摆出来,5 个 CRUD 接口逐个拆,让你看到 MVC 不是教科书三层等分,而是「Controller 接 HTTP + Service 写业务 + Module 组装」的工程分工。

理解这 7 个文件的协作,就能回答 NestJS 大厂面试的 80% 问题。剩下 20% 是 TypeORM、微服务、守卫拦截器这些进阶话题,不在本文范围。

文章目录

  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」
  • [NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理](#NestJS MVC 实战:用 hello 项目 7 个文件讲清分层、装饰器、依赖注入与错误处理)
    • [写在前面:为什么是 hello 项目](#写在前面:为什么是 hello 项目)
    • [一、7 个文件的全景图:谁干什么](#一、7 个文件的全景图:谁干什么)
    • [二、main.ts:工厂模式 + 启动入口](#二、main.ts:工厂模式 + 启动入口)
    • 三、AppModule:三件套组装根模块
    • [四、三层装饰器系统:NestJS 的灵魂](#四、三层装饰器系统:NestJS 的灵魂)
    • 五、AppController:依赖注入语法糖
      • [依赖注入链路 5 步(收藏资产)](#依赖注入链路 5 步(收藏资产))
    • [六、TodosController:5 个 CRUD 接口实战](#六、TodosController:5 个 CRUD 接口实战)
      • 路由匹配对照表(收藏资产)
      • [`@Body() vs @Body('field')` 对比](#@Body() vs @Body('field') 对比)
      • [`import { type TodoItem }` 是什么意思](#import { type TodoItem } 是什么意思)
    • [七、TodosService:业务逻辑 + 错误处理](#七、TodosService:业务逻辑 + 错误处理)
      • [重点 1:模拟数据库](#重点 1:模拟数据库)
      • [重点 2:`@Injectable()` 干什么](#重点 2:@Injectable() 干什么)
      • [重点 3:`NotFoundException` 错误处理(大厂面试高频)](#重点 3:NotFoundException 错误处理(大厂面试高频))
        • [`throw new Error` vs `throw new NotFoundException` 对比](#throw new Error vs throw new NotFoundException 对比)
      • [重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)](#重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产))
    • [八、完整请求链路图:从浏览器到 Service](#八、完整请求链路图:从浏览器到 Service)
    • 九、大厂面试高频问题清单
    • [十、自检清单:你真的看懂 hello 项目了吗](#十、自检清单:你真的看懂 hello 项目了吗)
    • 收尾:从「背文档」到「看懂项目」

一、7 个文件的全景图:谁干什么

hello 项目的目录结构如下(已省略配置文件):

text 复制代码
hello/
├── src/
│   ├── main.ts                  # 入口:工厂启动
│   ├── app.module.ts            # 根模块:三件套组装
│   ├── app.controller.ts        # 根控制器:处理 GET /
│   ├── app.service.ts           # 根服务:返回 Hello World
│   └── todos/
│       ├── Todos.module.ts      # 业务模块:组装 todos 三件套
│       ├── Todos.controller.ts  # 业务控制器:5 个 CRUD 接口
│       └── Todos.service.ts     # 业务服务:5 个 CRUD 方法
└── package.json

对应到 MVC 三层:

文件 MVC 角色 职责
main.ts 启动器 工厂创建应用 + 监听端口
*.module.ts 组装层 注册 Controller + Service,划分业务边界
*.controller.ts Controller 接 HTTP 请求、提参、调 Service、返响应
*.service.ts Service + Model 业务逻辑 + 数据存储(hello 用数组模拟)
AppService / TodosService Model 抽象 生产环境换成 TypeORM Repository

核心判断 :NestJS 的 V(View)不是 HTML 模板,而是 NestJS 自动把返回值 JSON.stringify 后写进 HTTP body;M(Model)在 hello 项目用模块级 let todos: TodoItem[] 数组模拟,生产换成 TypeORM。

二、main.ts:工厂模式 + 启动入口

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);
}
bootstrap();

4 个关键点

  1. NestFactory.create(AppModule) 是工厂模式落地------你不 new NestApplication(),而是把「怎么造应用」的细节交给工厂,调用方只关心「我要一个能跑的应用」。
  2. AppModule 是入口参数,意味着整个应用的依赖图都从这个根模块展开。
  3. process.env.PORT ?? 3000 用空值合并运算符兜底端口,云平台注入 PORT 环境变量时自动让位。
  4. bootstrap() 是 async 函数,调用时没 await------这是 Node 启动脚本的惯例,主线程跑完启动即可交出控制权。

面试角度:NestFactory.create 为什么是工厂模式?答:调用方不直接 new,由工厂统一创建并初始化依赖图,符合「开闭原则」,未来换底层实现(如 Fastify)不影响调用方。

三、AppModule:三件套组装根模块

typescript 复制代码
// src/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],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@Module 装饰器接收三个关键字段:

  • imports :引入其他模块,决定「哪些业务能被这个模块看到」。AppModule 导入 TodosModule,相当于把 todos 业务的依赖图挂到根上。
  • controllers :注册本模块的控制器类,NestJS 会扫描其中的 @Get/@Post 等装饰器建立路由表。
  • providers:注册本模块的 Service / Repository,被注册的类进入 IoC 容器,可被自动注入。

模块化好处(来自 main.ts 注释):如果一个文件几千行代码不行,所以要模块化;大型项目按业务划分 Module,NestJS 还会按需加载做性能优化。

四、三层装饰器系统:NestJS 的灵魂

hello 项目用到的装饰器分三类,这是大厂面试必问点

类别 装饰器 作用位置 干什么
类装饰器 @Module / @Controller / @Injectable 类声明前 给类附加元数据,标记「我是模块/控制器/可注入服务」
方法装饰器 @Get / @Post / @Put / @Delete 类方法前 把方法绑到 HTTP 路由
参数装饰器 @Param / @Body / @Query 方法参数前 从请求对象提取数据注入参数

readme.md 评价:「装饰器模式用到极致」。装饰器本质是「在不修改原有对象的前提下,动态添加额外功能」,用 @ 表示------可以理解成「给类贴一张说明书,NestJS 看到说明书就知道怎么处理它」。

五、AppController:依赖注入语法糖

typescript 复制代码
// src/app.controller.ts(运行未验证)
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

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

重点拆解 constructor 那一行------这是 TS 参数属性语法糖:

typescript 复制代码
constructor(private readonly appService: AppService) {}

等价于这三步:

typescript 复制代码
class AppController {
  private readonly appService: AppService;
  constructor(appService: AppService) {
    this.appService = appService;
  }
}

NestJS 看到这个构造参数类型是 AppService,去 IoC 容器找已注册的单例,自动传进来------程序员不用 new、不用手动传参。这就是依赖注入。

依赖注入链路 5 步(收藏资产)

text 复制代码
1. @Module providers:[AppService]    → 注册到 IoC 容器
2. @Injectable() 装饰 AppService 类  → 标记「我可被注入」
3. constructor 参数声明依赖           → 告诉容器「我需要 AppService」
4. 容器 new Controller 时自动传单例   → 解决依赖
5. this.appService.method() 调用    → 业务执行

面试金句 :依赖注入解决的是「组件之间耦合」的问题------Controller 不再 new Service,而是声明「我要什么」,由容器负责供给。好处是 Controller 和 Service 解耦,Service 可单测可替换。

六、TodosController:5 个 CRUD 接口实战

这是本文最核心的代码------一个文件覆盖 5 种 HTTP 方法 + 3 种参数装饰器。

typescript 复制代码
// src/todos/Todos.controller.ts(运行未验证)
import { Controller, Get, Post, Param, Body, Delete, Put } from '@nestjs/common';
import { TodosService } from './Todos.service';
import { type TodoItem } from './Todos.service';

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

  @Get()
  findAll(): TodoItem[] {
    return this.todosService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): TodoItem {
    return this.todosService.findOne(Number(id));
  }

  @Post()
  create(@Body('title') title: string): TodoItem {
    return this.todosService.create(title);
  }

  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: `删除 todoItem ${id}` };
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() patch: Partial<TodoItem>): TodoItem {
    return this.todosService.update(Number(id), patch);
  }
}

路由匹配对照表(收藏资产)

HTTP 方法 装饰器组合 完整路径 调用方法
GET @Controller('todos') + @Get() GET /todos findAll
GET @Controller('todos') + @Get(':id') GET /todos/:id findOne
POST @Controller('todos') + @Post() POST /todos create
DELETE @Controller('todos') + @Delete(':id') DELETE /todos/:id remove
PUT @Controller('todos') + @Put(':id') PUT /todos/:id update

关键细节

  1. 路由拼接规则 :完整路径 = @Controller(prefix) + @Method(subpath)@Controller('todos') 是类级前缀,所有方法共享。
  2. RESTful 标准 (注释原话:restful 暴露资源的统一标准):同一资源用 HTTP 方法区分操作,URL 是名词不是动词。
  3. URL 参数永远是字符串@Param('id') 拿到的是 string 类型,必须 Number(id)number。如果用户访问 /todos/abc,会得到 NaNArray.find 返回 undefined,最终抛 NotFoundException → HTTP 404。

@Body() vs @Body('field') 对比

用法 提取内容 类型 配合 TS
@Body('title') title: string 只取 body 中的 title 字段 单字段 直接指定类型
@Body() patch: Partial<TodoItem> 注入整个请求体对象 整个 body 配合 Partial<T>

Partial<TodoItem>TodoItem 所有属性变可选,方便 PATCH 语义(只更新传入字段)。但要注意:这会让 id 也可被改 ,存在 OWASP Mass Assignment 漏洞。生产建议用 Pick<TodoItem, 'title' | 'complete'> 限制可改字段。

import { type TodoItem } 是什么意思

typescript 复制代码
import { type TodoItem } from './Todos.service';

这是 TS 的 import type 语法。原因:当 tsconfig 同时开 isolatedModulesemitDecoratorMetadata 时,被装饰器参数引用的 interface 必须用 import type,否则报 TS1272: A type referenced in a decorated signature must be imported with 'import type'

七、TodosService:业务逻辑 + 错误处理

typescript 复制代码
// src/todos/Todos.service.ts(运行未验证)
import { Injectable, NotFoundException } from '@nestjs/common';

export interface TodoItem {
  id: number;
  title: string;
  complete: boolean;
}

let todos: TodoItem[] = [
  { id: 1, title: '学习 NestJS', complete: false },
  { id: 2, title: '学习 CRUD', complete: true },
];
let nextId = 3;

@Injectable()
export class TodosService {
  findAll(): TodoItem[] {
    return todos;
  }

  findOne(id: number): TodoItem {
    const todoItem = todos.find(t => t.id === id);
    if (!todoItem) throw new NotFoundException(`TodoItem ${id} 不存在`);
    return todoItem;
  }

  create(title: string): TodoItem {
    const todoItem: TodoItem = { id: nextId++, title, complete: false };
    todos.push(todoItem);
    return todoItem;
  }

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

  update(id: number, patch: Partial<TodoItem>): TodoItem {
    const todoItem = this.findOne(id);
    Object.assign(todoItem, patch);
    return todoItem;
  }
}

重点 1:模拟数据库

let todos: TodoItem[]let nextId = 3 是模块级变量,重启会丢失。生产环境换成 TypeORM 的 Repository,写法变化不大------这是 NestJS 分层的好处,存储层换底层不动业务逻辑。

重点 2:@Injectable() 干什么

注释原话:「把一个普通类变成 Nest 容器可管理的服务,从而能被自动实例化和注入到任何需要它的地方」。换句话说,加了 @Injectable(),这个类的实例就能被 IoC 容器统一管理,默认是单例

重点 3:NotFoundException 错误处理(大厂面试高频)

findOne 找不到数据时抛 throw new NotFoundException(...),NestJS 自动转成 HTTP 404 响应,body 类似:

json 复制代码
{
  "statusCode": 404,
  "message": "TodoItem 999 不存在",
  "error": "Not Found"
}
throw new Error vs throw new NotFoundException 对比
写法 HTTP 状态码 语义 推荐度
throw new Error('出错了') 500 服务器内部错误 ❌ 语义错误
throw new NotFoundException('出错了') 404 资源不存在 ✅ 语义正确

面试金句:查不到数据是「客户端的锅」(请求了不存在的资源),应该返回 404;如果返回 500 等于把锅甩给服务器,会让前端误判是后端崩了。

重点 4:NestJS 内置 HTTP 异常类对照表(收藏资产)

异常类 状态码 使用场景
BadRequestException 400 参数校验失败
UnauthorizedException 401 未登录
ForbiddenException 403 登录但无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突(如重复注册)
InternalServerErrorException 500 真正的服务器错误

所有类都继承自 HttpException,NestJS 拦截到这些异常会自动序列化成标准 JSON 响应。

这就是 readme.md 提到的「NestJS 提供各种错误类,标准化错误输出------status code 状态码 + message 消息」。面试题「请说下你是如何处理后端报错的」答案就在这里:用语义化异常类,让 NestJS 框架统一兜底。

八、完整请求链路图:从浏览器到 Service

POST /todos 为例(来自 Todos.controller.ts 文件底部 ASCII 注释):

text 复制代码
前端  POST /todos   body: {"title":"学习 React"}
  ↓
NestJS 路由匹配:@Controller('todos') + @Post()
  ↓
进入 create,@Body('title') 提取出 title = "学习 React"
  ↓
this.todosService.create("学习 React")
  ↓
service: 生成新对象 {id:3, title:"学习 React", complete:false}
  ↓
push 进 todos 数组
  ↓
NestJS 把返回值 JSON.stringify 后写入 HTTP body
  ↓
浏览器收到 201 + {"id":3,"title":"学习 React","complete":false}

根路径 GET / 的链路(来自 readme.md):

text 复制代码
浏览器 GET http://localhost:3000
  ↓
main.ts app.listen(3000) 接收
  ↓
路由匹配 GET /
  ↓
找到 AppController(由 AppModule 注册)
  ↓
AppController 的 @Get() 装饰器匹配
  ↓
调用 getHello()
  ↓
调用 this.appService.getHello()
  ↓
AppService.getHello() 返回 'Hello World!'
  ↓
浏览器显示 Hello World!

九、大厂面试高频问题清单

整理 8 个高频考点,对应文中事实编号:

  1. NestJS MVC 三层在 hello 项目怎么落地 ------M 是数组模拟、V 是 NestJS 自动 JSON、C 是 @Controller + HTTP 方法装饰器。
  2. NestJS 请求处理流程------浏览器 → main.ts → 路由匹配 → Controller → Service → JSON 响应。
  3. 三类装饰器分别干什么------类装饰器附加元数据、方法装饰器绑路由、参数装饰器提参。
  4. 依赖注入链路 5 步 ------providers 注册 → @Injectable 标记 → constructor 声明 → 容器传单例 → 调用方法。
  5. @Body() vs @Body('field') 区别 ------整体注入 vs 单字段提取,前者配合 Partial<T>,后者适合只要单字段。
  6. URL 参数为什么是字符串 ------HTTP 协议层就是文本,/todos/abc 拿到的是 'abc',需要 Number() 转换,转换失败得 NaN,最终触发 404。
  7. NestJS 内置错误类解决什么问题------标准化错误输出(status + message),让前端按状态码分支处理。
  8. throw new Error vs throw new NotFoundException------500 vs 404,语义正确性决定接口设计质量。

十、自检清单:你真的看懂 hello 项目了吗

读完本文,对照 8 个自检点,能答 6 个以上才算真懂:

  • NestFactory.create 为什么是工厂模式?调用方不直接 new,由工厂统一创建。
  • @Moduleimports / controllers / providers 分别装什么?
  • constructor(private readonly appService: AppService) 这一行展开是几步?3 步。
  • @Controller('todos') + @Get(':id') 匹配的完整路径是什么?GET /todos/:id
  • @Param('id') 拿到的是什么类型?string,必须 Number() 转。
  • @Body('title')@Body() 区别是什么?单字段 vs 整体注入。
  • NotFoundException 对应 HTTP 几?404。
  • Partial<TodoItem> 有什么安全风险?让 id 也可被改,存在 Mass Assignment 漏洞。

收尾:从「背文档」到「看懂项目」

NestJS 的学习陷阱在于:装饰器语法看起来简单,但理解「为什么这么设计」比记住「怎么写」重要得多。hello 项目 7 个文件 + 5 个 CRUD 接口把这个「为什么」完整跑通------main.ts 工厂启动、AppModule 三件套组装、TodosController 5 种 HTTP 方法装饰器、TodosService 5 个 CRUD 方法 + NotFoundException 容错。每个概念都能在真实代码里找到对应位置,比单独看装饰器、依赖注入、异常处理的孤岛文档要扎实得多。

下一步建议:把 hello 项目 clone 下来跑一遍,再用 Postman 打 5 个接口,对照本文的链路图看 console.log 输出。当你能凭记忆画出「浏览器 → Controller → Service → JSON 响应」的完整链路,NestJS 大厂面试的 MVC + 装饰器 + DI + 错误处理这四大考点就稳了。

进阶方向(不在本文范围):TypeORM 替换数组存储、自定义 ExceptionFilter 统一错误格式、Guard 守卫做鉴权、Interceptor 拦截器做日志。这些是 hello 项目跑通后的下一站。

标签:NestJS,MVC 架构,依赖注入,装饰器,后端面试

相关推荐
m0_3807438711 小时前
从零调用 Claude 教程
开发语言·python·node.js
烬羽15 小时前
从 nest new 到 Hello World:一个最小 NestJS 项目,讲透工厂 + 装饰器 + 模块化
设计模式·node.js·nestjs
渔夫正在掘金17 小时前
Cordis 中文教程:渐进式构建插件化应用
前端·node.js·ai编程
大家的林语冰20 小时前
✌️ 让 Rust 再次伟大,pnpm 12 抛弃 TypeScript,移植 Rust 原地起飞!
前端·javascript·node.js
抓不住时间的沙1 天前
butterfly主题美化,打造属于自己的个性博客
java·开发语言·前端·javascript·node.js·github
爱敲键盘的猴子1 天前
Spring MVC 详解(一):全注解开发与请求响应处理
java·spring·mvc
抓不住时间的沙1 天前
N1搭建Hexo个人博客,部署到Github
python·docker·node.js·debian·github·arm
必须会一定会1 天前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
人工智能·node.js·ai编程
weixin_431600442 天前
前端数据埋点(1):一次点击如何变成一条埋点
前端·js·数据埋点