主题
项目实践
以包含管理员和参与者角色的 Hackathon API 为例
课程项目将 NestJS 的基础组件组合为一个生产风格 API:Prisma 管理 PostgreSQL 数据,Better Auth 维护会话,Arcjet 在入口处理安全策略,业务模块实现用户和 Hackathon 的用例。
业务域
| 角色 | 能力 |
|---|---|
admin | 创建、更新、删除和查询 Hackathon。 |
participant | 查询处于可参与状态的 Hackathon,并报名参与。 |
角色仅定义大致权限边界。报名时还需要检查活动状态、重复报名、容量和截止时间等业务规则。
课程中的 User Module 有两个受保护的读取端点:GET /users/all 仅 admin 可调用;GET /users/:id 对任意已认证用户开放。找不到用户由 Service 抛出 NotFoundException,Controller 不直接访问 Prisma。
目录组织
bash
src
├── auth
│ ├── auth.module.ts
│ └── guards
├── prisma
│ ├── prisma.module.ts
│ └── prisma.service.ts
├── users
│ ├── dto
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
└── hackathons
├── dto
├── hackathons.controller.ts
├── hackathons.module.ts
└── hackathons.service.ts每个业务模块只导出其他模块确实需要的 Provider。auth、prisma、日志等基础设施可以作为全局模块;users、hackathons 等业务模块优先显式导入。
Controller、DTO 与 Service
Controller 声明路由、DTO 和访问策略;Service 完成业务用例;Prisma Service 仅负责数据库访问。不要让 Controller 直接调用 ORM,也不要用数据库 Model 代替外部输入 DTO。
ts
@Controller('hackathons')
export class HackathonsController {
constructor(private readonly hackathonsService: HackathonsService) {}
@Post()
@Roles('admin')
create(@Body() input: CreateHackathonDto) {
return this.hackathonsService.create(input)
}
@Post(':id/join')
@Roles('participant')
join(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUser) {
return this.hackathonsService.join(id, user.id)
}
}CreateHackathonDto 与 UpdateHackathonDto 应分别描述可创建、可更新字段。更新 DTO 通常由创建 DTO 派生为可选字段,但服务层仍需验证状态转换是否允许。
课程的创建 DTO 包含 name、可选 description、startsAt、endsAt 和可选 isActive:名称最少 3 个字符,描述长度 10–1000,日期必须是未来时间。日期字符串需经 @Type(() => Date) 转为 Date 后,再配合 @IsDate() 和项目定义的未来日期规则校验。
数据关系与报名
Hackathon 归属于创建者;HackathonParticipant 记录“哪个用户在何时加入哪个 Hackathon”。数据库层应限制同一用户不能重复加入同一活动:
prisma
model HackathonParticipant {
id String @id @default(uuid())
hackathonId String
userId String
joinedAt DateTime @default(now())
@@unique([hackathonId, userId])
}POST /hackathons/:id/join 只允许 participant。Service 的顺序是:查找活动 → 检查 isActive 与结束时间 → 检查或依赖唯一约束阻止重复报名 → 创建报名记录。不存在返回 404,已结束、未激活或重复报名返回 400。
写操作与事务
报名等操作会同时读取活动、检查资格并创建关联记录,应放在事务中。数据库唯一约束保证并发情况下不会重复报名;Service 捕获已知约束冲突并转换为合适的 HTTP 异常。
ts
await this.prisma.$transaction(async (tx) => {
const hackathon = await tx.hackathon.findUnique({ where: { id } })
if (!hackathon?.isOpen) throw new BadRequestException('活动不可参与')
return tx.registration.create({
data: { hackathonId: id, userId },
})
})事务只包裹数据库工作;邮件、文件上传、HTTP 调用等外部副作用应通过事件或队列在事务提交后执行。
统一响应
响应 Interceptor 可统一添加 data、请求 ID、分页元数据等字段。它只处理成功响应;错误响应仍由 Exception Filter 处理。接口约定应覆盖成功和错误两条路径,避免客户端需要猜测不同端点的格式。
课程在此基础上增加 @ResponseMessage():它用 SetMetadata 为单个路由定义成功消息,Interceptor 用 Reflector 读取该元数据,未设置时回退为 Success。写操作可返回 Hackathon created successfully 等消息,读取端点沿用默认值。
Agentic 开发
课程以 Agent 协助生成样板代码。AGENTS.md / CLAUDE.md 放在仓库根目录,约束 Agent 采用 NestJS 模式、使用构造函数注入、按任务匹配加载 Skill,并在新会话恢复上下文。适合委托给 Agent 的内容包括模块脚手架、DTO 初稿和重复 CRUD;开发者仍需负责:
- 明确领域边界、权限矩阵和不可变业务规则。
- 审查生成代码中的认证、授权、输入校验和事务边界。
- 运行单元测试、端到端测试和数据库迁移检查。
- 不向 Agent 提供生产密钥、真实用户数据或未脱敏日志。
在让 Agent 实现 Arcjet、Prisma 或 Better Auth 前,先要求它读取对应的集成文档或 Skill、给出方案,再实施和运行构建;不能因为 Agent 能生成代码就跳过权限、迁移和错误处理审查。
交付检查
- 管理员和参与者分别验证允许与拒绝的请求。
- 为登录状态、角色、资源归属和活动状态覆盖端到端测试。
- 检查分页上限、排序白名单、错误响应和统一响应格式。
- 审查迁移 SQL、唯一约束和事务中的并发行为。
- 先观察限流与机器人规则,再启用阻断模式。
