综合练习
这一课你将能做到
- 交付一个模块化的刷题平台 API:题库 CRUD、条件筛选、作答判分、成绩统计,校验、鉴权、异常、配置、测试齐备
- 用结业清单逐项自检,确认本轨道 12 节课的能力闭环
- 用自己的话写出 NestJS vs Express vs Spring Boot 的选型结论
为什么要学
前面每节课只教一个零件:Module、Controller、Service、Pipe、Guard、Config、异常、测试。本课要求你组装。
判断是否真学会一个框架,从来不是「看得懂示例」,而是能否从零搭出结构正确的项目,并处理好失败路径——非法输入 400、未登录 401、权限不足 403、资源不存在 404。本课做完,你应该能向同事演示一个接口契约稳定、测试可红可绿的完整后端。
刻意延续「做题平台」场景:同一套业务,本站 Java 侧用 Spring Boot 实现过,Node 侧你用 Nest 再实现一遍。对比之下你会真正理解——框架换的是骨架,业务本身从没变。
先建立图像
目标接口契约:
| 方法与路径 | 访问规则 | 成功状态 |
|---|
GET /api/health | 公开 | 200 |
GET /api/questions | 公开,支持 difficulty/tag/page | 200 |
GET /api/questions/:id | 公开 | 200 / 404 |
POST /api/questions | 管理员 | 201 / 400 / 403 |
PATCH /api/questions/:id | 管理员 | 200 / 404 |
DELETE /api/questions/:id | 管理员 | 204 / 404 |
POST /api/submissions | 登录学生 | 201 / 400 / 401 |
GET /api/submissions/me | 登录学生 | 200 |
GET /api/submissions/me/stats | 登录学生 | 200 |
选型对照表:
| 场景 | 更合适的选择 |
|---|
| 脚本、极简原型、轻量 webhook | Express |
| TS 团队中型 API、BFF 聚合层 | NestJS |
| 本站企业级主 API、复杂事务与生态 | Spring Boot |
| 全栈页面加极简服务端逻辑 | Next.js Route Handler |
跟着做
步骤 1:冻结需求与目录结构
沿用本轨道的 quiz-api,最终目录应近似:
src/
main.ts
app.module.ts
config/
jwt.config.ts
common/
filters/all-exceptions.filter.ts
exceptions/business.exception.ts
auth/
auth.module.ts
auth.controller.ts
auth.service.ts
guards/jwt-auth.guard.ts
guards/roles.guard.ts
decorators/ (public / roles / current-user)
questions/
questions.module.ts
questions.controller.ts
questions.service.ts
dto/ (create / update)
submissions/
submissions.module.ts
submissions.controller.ts
submissions.service.ts
dto/submit-answer.dto.ts
health/health.controller.ts
test/
app.e2e-spec.ts
步骤 2:统一装配全局能力
建议新建 src/setup-app.ts,把全局装配抽成函数,main.ts 与 e2e 共用:
import {
INestApplication,
ValidationPipe,
} from "@nestjs/common";
export function setupApp(app: INestApplication): INestApplication {
app.setGlobalPrefix("api");
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.enableShutdownHooks();
return app;
}
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { setupApp } from "./setup-app";
async function bootstrap() {
const app = setupApp(await NestFactory.create(AppModule));
const config = app.get(ConfigService);
await app.listen(config.get<number>("PORT", 3000));
}
bootstrap();
全局过滤器通过 AppModule 的 APP_FILTER 注册,两个 Guard 通过 APP_GUARD 注册。
步骤 3:实现题库主路径
QuestionsService 使用内存数组(沿用 Service 课代码),保证:
findOne 未命中 → NotFoundException(404)。
create 中 answerIndex 越界 → BadRequestException(400)。
- 题干重复 →
ConflictException(409)。
- 列表的 difficulty/tag 过滤与分页在
search 中一次完成。
健康检查独立成 HealthController 并贴 @Public():
import { Controller, Get } from "@nestjs/common";
import { Public } from "../auth/decorators/public.decorator";
@Controller("health")
export class HealthController {
@Public()
@Get()
check() {
return { ok: true, uptime: process.uptime() };
}
}
步骤 4:完成作答与统计接口
SubmissionsController 挂全局 Guard 后默认需要登录,用 @CurrentUser() 取人:
import { Body, Controller, Get, Post } from "@nestjs/common";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
import { SubmitAnswerDto } from "./dto/submit-answer.dto";
import { SubmissionsService } from "./submissions.service";
@Controller("submissions")
export class SubmissionsController {
constructor(private readonly submissions: SubmissionsService) {}
@Post()
submit(@Body() dto: SubmitAnswerDto, @CurrentUser() user) {
return this.submissions.submit(dto.questionId, user.id, dto.selectedIndex);
}
@Get("me/stats")
myStats(@CurrentUser() user) {
return this.submissions.stats(user.id);
}
@Get("me")
mine(@CurrentUser() user) {
return this.submissions.listByUser(user.id);
}
}
注意路由顺序:me、me/stats 是静态段,没有 :id 冲突问题;将来若加详情路由,静态段必须写在参数段之前。
步骤 5:核对鉴权矩阵
逐一验证规则:公开接口贴 @Public();管理员接口 @Roles("admin");学生接口只要登录。用两个账号各打一遍:
# 管理员登录
curl -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"admin123\"}"
# 学生登录
curl -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d "{\"username\":\"student1\",\"password\":\"123456\"}"
预期:学生 Token 创建题目 → 403;学生 Token 提交作答 → 201;无 Token 提交 → 401。
步骤 6:补齐测试底线
- QuestionsService 单测:命中、未命中 404、重复题干 409、answerIndex 越界 400。
- SubmissionsService 单测:判分正确、stats 正确率(mock QuestionsService)。
- e2e:
GET /api/questions 200、GET /api/questions/999 404。
- e2e:未登录提交 401、学生建题 403、非法题体 400。
npm test
npm run test:e2e
步骤 7:写选型三段论(必须原创)
- 如果重做本站 Spring Boot 的题库模块,什么条件下你会用 Nest?
- 什么需求会让你退回 Express?
- 结合本站实际,核心交易与题库系统为什么仍交给 Spring Boot?
好的回答会提到团队语言、协作规模、生态与事务需求,而不是「谁火用谁」。
步骤 8:可选打磨
npm install @nestjs/swagger,用 @ApiTags / @ApiProperty 生成 OpenAPI 文档页。
- 写 Dockerfile:先
npm run build,运行 node dist/main。
- 在路线图上勾选 NestJS 节点,回顾整条 TS 后端学习路径。
步骤 9:提交前对照检查表
- 九个端点的路径、方法、状态码与契约表一致。
- DTO 加全局 ValidationPipe 能拦非法输入,未知字段被拒绝。
- 404/401/403/409 都有自动化测试覆盖。
- ConfigModule 管理 PORT、JWT_SECRET,
.env 未进 Git。
- 全局异常过滤器输出统一 JSON,500 不含堆栈。
- 选型三段论已用自己的话写进笔记。
讲清楚要点
- 综合练习考的是装配:全局 Pipe、Filter、前缀、Guard 必须真实注册,而不是停留在单课笔记里。
- 接口契约是产品:状态码与错误外壳要写到测试中,而不是只靠口头约定。
- 分层是贯穿主线:Controller 不写业务与 SQL,Service 不碰请求对象。
- 测试是交付门槛:最低单测加 e2e 通过,重构才有安全网。
- 选型服务于场景:语言、规模、生态决定框架,不存在「一个框架淘汰全部」。
- 本站语境要清晰:Spring Boot 仍是主栈,Nest 承担 TS 后端与 BFF 角色,二者长期共存。
你可能会懵的地方
-
和 Express 版做同样的业务,算重复劳动吗?
刻意重复才有体感:同一业务两套骨架并排对比,DI 和 Module 带来的收益才看得见。
-
结业必须接真实数据库吗?
不必。内存版可过关;分层正确比 ORM 花活更重要。将来按数据库课的替换点切换,Controller 一行不改。
-
做到什么程度算入门完成?
初学者按硬指标判断:契约表全部打通 + 四类失败有处理 + 测试底线全绿 + 能解释选型。Swagger、Docker、Passport 都是加分项。
-
时间紧怎么切?
先垂直打通一条:题库列表加单测;再补创建、校验、e2e;最后加鉴权。垂直切片优于横向铺开。
-
Next.js 的 Route Handler 和 Nest 冲突吗?
极简页面级 BFF 用 Route Handler 足够;接口一多、需要统一鉴权与模块边界时,独立 Nest 服务更清晰。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 只实现 Happy Path | 失败行为全未定义 | 补 400/401/403/404 契约与测试 |
| 所有类塞进 AppModule | 无法扩展与协作 | 按业务域拆特征模块 |
| 没有测试就宣称完成 | 回归全靠运气 | 单测 + e2e 底线 |
| 写下「Nest 全面替代 Spring」 | 脱离本站团队与生态 | 按语言与场景选型 |
| e2e 装配与 main.ts 不一致 | 测试假绿 | 抽 setupApp 公共函数 |
卡点急救
- 某个接口行为和契约表不符:回到该层排查——先看 Guard 规则,再看 Pipe 校验,最后看 Service 分支,逐层定位。
- 测试数据互相干扰:内存单例在用例间残留;beforeEach 重建模块或显式重置数组。
- 不确定是否可以结业:不要凭感觉,逐项过步骤 9 的检查表;全部打勾即完成,否则继续补缺。
面试常问
-
完整介绍一下你用 NestJS 做的项目。
按「业务域 → 模块划分 → 请求链路(Guard/ Pipe/ Controller/ Service)→ 失败处理与统一异常 → 测试策略」组织,再补一句为什么选 Nest(团队 TS、规模适中),就是一份合格的项目陈述。
-
NestJS 相对 Express 多买到了什么?
强制模块边界、DI 容器、Guard/Interceptor/Pipe 标准扩展点、以及可测试的装配方式;代价是概念和样板更多。对小型脚本这些是负担,对多人中型项目是资产。
-
同一个业务用 Spring 和 Nest 各做一遍,你体会到什么?
语言和注解不同,但分层思想同构:都是 HTTP 适配层转调业务层、横切逻辑前置、异常统一收口。框架能力的本质是约束与复用,业务模型与语言无关。
自测三问
- 你的全局 ValidationPipe 挂在哪里、用了哪三个选项?
- 列出三条被自动化测试覆盖的契约(含状态码)。
- 用自己的话说:Nest 相对 Express 多买到了什么、代价是什么?
小结一句话
Nest 轨道的闭环是:模块化 REST 加校验、鉴权、配置、异常与测试;框架服务场景,而不是信仰。
延伸
javascript-core → typescript-essentials → nodejs-essentials → nestjs-essentials
下一步可选:Docker 容器化部署、CI/CD 流水线,或用 GraphQL / WebSocket 扩展刷题平台的实时能力。