Skip to content

项目实践

以包含管理员和参与者角色的 Hackathon API 为例

课程项目将 NestJS 的基础组件组合为一个生产风格 API:Prisma 管理 PostgreSQL 数据,Better Auth 维护会话,Arcjet 在入口处理安全策略,业务模块实现用户和 Hackathon 的用例。

业务域

角色能力
admin创建、更新、删除和查询 Hackathon。
participant查询处于可参与状态的 Hackathon,并报名参与。

角色仅定义大致权限边界。报名时还需要检查活动状态、重复报名、容量和截止时间等业务规则。

课程中的 User Module 有两个受保护的读取端点:GET /users/alladmin 可调用;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。authprisma、日志等基础设施可以作为全局模块;usershackathons 等业务模块优先显式导入。

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)
  }
}

CreateHackathonDtoUpdateHackathonDto 应分别描述可创建、可更新字段。更新 DTO 通常由创建 DTO 派生为可选字段,但服务层仍需验证状态转换是否允许。

课程的创建 DTO 包含 name、可选 descriptionstartsAtendsAt 和可选 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、唯一约束和事务中的并发行为。
  • 先观察限流与机器人规则,再启用阻断模式。

基于 MIT 许可发布