返回笔记首页

第 06 集:用 Cursor Agent 从 0 搭 NestJS 项目

主题配置

一、本集目标

用 Cursor Agent 全程驱动,从空项目出发,完成一个生产可用的 NestJS 用户管理模块,包含:

  • Prisma Schema 定义
  • CreateUserDto、UpdateUserDto、FindUsersDto(含参数校验)
  • UserService(完整 CRUD + 软删除 + bcrypt 密码加密)
  • UserController(含 Swagger 注解)
  • UserModule 注册
  • Swagger 文档验证可访问
  • UserService 单元测试

二、Agent 驱动开发的核心原则

2.1 先确认需求,再让 AI 写代码

让 Agent 先分析需求文档、复述理解内容,你确认没有歧义之后再让它开始实现。

原因:Agent 一旦开始写代码,中途改需求的成本比开始前确认高得多。

2.2 把需求写成文档,不要在 Prompt 里口述

把需求写成 PRD.md 文件,通过 @PRD.md 引用。

原因:

  • 文档可以随时修改补充
  • Agent 能完整读取,不会遗漏
  • 后续其他集的演示可以复用同一份文档

2.3 一次生成后必须审查,不能直接用

Agent 生成完代码,第一件事是逐文件检查,不是直接运行。

重点审查的逻辑:

  • 密码是否用 bcrypt 加密(而不是明文存储)
  • 查询返回值是否排除了 password 字段
  • 软删除是否用 update deletedAt 而不是 delete
  • 事务逻辑是否正确

2.4 发现问题,用 Agent 修,不要手动改

在 Chat 里说"这里有个问题:xxx,帮我修复",让 Agent 修正。这样整个过程保持 AI 驱动,也顺带演示了"审查 + 修正"的完整工作流。


三、需求文档:PRD.md

在项目根目录创建 PRD.md,内容如下:

markdown
# 用户管理模块需求文档

## 数据字段

| 字段 | 类型 | 说明 |
|------|------|------|
| id | 自增整数 | 主键 |
| username | 字符串 | 用户名,唯一,3~20 个字符 |
| email | 字符串 | 邮箱,唯一 |
| password | 字符串 | 密码,存储时必须用 bcrypt 加密,salt rounds = 10 |
| role | 枚举 | USER \| ADMIN,默认 USER |
| isActive | 布尔 | 是否激活,默认 true |
| createdAt | 时间 | 创建时间,自动生成 |
| updatedAt | 时间 | 更新时间,自动更新 |
| deletedAt | 时间(可空)| 软删除时间,null 表示未删除 |

## 接口列表

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/users | 创建用户(注册) |
| GET | /api/users | 获取用户列表(分页 + username 搜索) |
| GET | /api/users/:id | 获取单个用户详情 |
| PATCH | /api/users/:id | 更新用户信息 |
| DELETE | /api/users/:id | 软删除用户 |

## 业务规则

- 密码存储前必须用 bcrypt 加密,salt rounds = 10
- 所有查询的返回值必须排除 password 字段
- 软删除:DELETE 接口只更新 deletedAt 字段,不真正删除记录
- 查询列表时自动过滤 deletedAt 不为 null 的记录
- 分页:默认第 1 页,每页 10 条
- 返回格式统一:{ code: 0, data: any, message: "ok" }
- 所有接口加 Swagger 注解

## 技术约束

- ORM:Prisma
- 参数校验:class-validator
- 异常类:使用 NestJS 内置异常类

四、演示操作步骤

第一步:初始化项目

bash
# 创建 NestJS 项目
npx @nestjs/cli new agent-demo
cd agent-demo

# 安装必要依赖
npm install @prisma/client prisma
npm install @nestjs/swagger swagger-ui-express
npm install class-validator class-transformer
npm install @nestjs/config
npm install bcrypt
npm install --save-dev @types/bcrypt

# 初始化 Prisma
npx prisma init

执行完后项目结构:

plain
agent-demo/
├── prisma/
│   └── schema.prisma    ← 接下来要修改这个文件
├── src/
│   ├── app.module.ts
│   └── main.ts
└── .env                 ← 配置数据库连接

第二步:配置 .env

打开 .env,修改 DATABASE_URL

plain
DATABASE_URL="postgresql://postgres:你的密码@localhost:5432/agent_demo"

在 PostgreSQL 里创建数据库:

sql
CREATE DATABASE agent_demo;

验证连接:

bash
npx prisma db pull   # 如果能连上会有输出,连不上会报错

第三步:配置 .cursorrules

在项目根目录创建 .cursorrules,写入第 05 集的 NestJS 模板内容(完整内容见第 05 集文档)。

第四步:创建 PRD.md

把第三章的 PRD 内容写入项目根目录的 PRD.md 文件。

第五步:AI 需求确认

在 Cursor Chat(Cmd+L)里输入:

plain
@PRD.md 请阅读这份需求文档,然后:
1. 告诉我你理解的功能点有哪些
2. 列出你准备创建哪些文件
3. 有没有模糊或有歧义的地方需要我补充说明

检查 AI 的复述是否和需求一致,重点确认:

  • 它知道要用 bcrypt 加密密码
  • 它知道查询返回值要排除 password
  • 它知道 DELETE 是软删除
  • 它列出的文件清单里有没有遗漏

如果有遗漏(比如没提 common/dto/response.dto.ts),在 Chat 里补充:

plain
你的文件列表里漏了 src/common/dto/response.dto.ts,
这是所有接口返回的统一格式,需要提前创建

第六步:生成 Prisma Schema

切换到 Agent 模式(Cmd+I),输入:

plain
根据 @PRD.md 里的用户字段需求,完成 prisma/schema.prisma 文件里的 User model 定义。

要求:
- 字段完全按需求文档来
- role 用 enum 实现:enum Role { USER ADMIN }
- deletedAt 必须是可空类型(DateTime?)
- createdAt 用 @default(now())
- updatedAt 用 @updatedAt

Agent 修改 schema.prisma 后,检查生成的内容:

plain
# 期望看到的 User model 结构
model User {
  id        Int       @id @default(autoincrement())
  username  String    @unique
  email     String    @unique
  password  String
  role      Role      @default(USER)
  isActive  Boolean   @default(true)
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}

enum Role {
  USER
  ADMIN
}

确认无误后运行 migration:

bash
npx prisma migrate dev --name init-user
npx prisma generate

两个命令都成功执行(无报错)才继续下一步。

第七步:生成完整 UserModule

在 Agent 模式输入:

plain
根据 @PRD.md 和 @prisma/schema.prisma,帮我实现完整的 UserModule。

需要创建以下文件:
1. src/common/dto/response.dto.ts
2. src/modules/user/dto/create-user.dto.ts(用 class-validator 校验)
3. src/modules/user/dto/update-user.dto.ts
4. src/modules/user/dto/find-users.dto.ts(分页 + 搜索参数)
5. src/modules/user/user.service.ts
6. src/modules/user/user.controller.ts
7. src/modules/user/user.module.ts

额外要求:
- 密码用 bcrypt.hash(password, 10) 加密后再存
- 所有查询返回值用 Prisma 的 select 排除 password 字段
- findAll 用 where: { deletedAt: null } 过滤软删除记录
- remove 方法用 prisma.user.update 更新 deletedAt,不用 prisma.user.delete
- 分页用 skip: (page - 1) * pageSize, take: pageSize

等待 Agent 执行完成。

第八步:代码审查

Agent 完成后,按以下顺序逐文件检查:

检查**response.dto.ts**

typescript
# 期望看到
export class ResponseDto<T> {
  code: number;
  data: T;
  message: string;
}

检查**create-user.dto.ts**

期望看到的校验装饰器:

  • @IsString() + @MinLength(3) + @MaxLength(20) 在 username 上
  • @IsEmail() 在 email 上
  • @IsString() + @MinLength(6) 在 password 上
  • @IsEnum(Role) + @IsOptional() 在 role 上

检查 **user.service.ts** 的关键逻辑

方法 检查项 期望
create 密码加密 bcrypt.hash(password, 10)
create 返回值 select
排除 password
findAll 过滤软删除 where: { deletedAt: null }
findOne 找不到时 NotFoundException
remove 删除方式 update({ data: { deletedAt: new Date() } })
如果发现问题(以 remove 方法用了硬删除为例)

在 Chat 里输入:

plain
user.service.ts 里的 remove 方法用的是 prisma.user.delete(),
这是硬删除。需要改成软删除:用 prisma.user.update 把 deletedAt 更新为当前时间

等 Agent 修复后再继续。

检查**user.controller.ts**

  • 每个方法是否有 @ApiTags@ApiOperation
  • 方法参数是否用了对应的 DTO
  • 路径是否是 kebab-case

第九步:注册模块

在 Agent 模式输入:

plain
帮我在 app.module.ts 里注册 UserModule,
同时配置好 ConfigModule(全局)和 PrismaService

Agent 修改完后,检查 app.module.ts

typescript
# 期望看到
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    UserModule,
  ],
})
export class AppModule {}

第十步:配置 Swagger

main.ts 里加入 Swagger 配置(如果 Agent 没有加,手动加):

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 全局参数校验
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

  // Swagger 配置
  const config = new DocumentBuilder()
    .setTitle('Agent Demo API')
    .setDescription('用户管理模块接口文档')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

第十一步:启动验证

bash
npm run start:dev

启动成功后,打开浏览器访问:

plain
http://localhost:3000/api

Swagger 文档里应该能看到 Users 分组下的 5 个接口:

  • POST /api/users
  • GET /api/users
  • GET /api/users/{id}
  • PATCH /api/users/{id}
  • DELETE /api/users/{id}

在 Swagger 里测试 POST /api/users

json
{
  "username": "testuser",
  "email": "test@example.com",
  "password": "123456"
}

期望返回:

json
{
  "code": 0,
  "data": {
    "id": 1,
    "username": "testuser",
    "email": "test@example.com",
    "role": "USER",
    "isActive": true,
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-01T00:00:00.000Z",
    "deletedAt": null
  },
  "message": "ok"
}

注意:返回值里不应该有 password 字段。

第十二步:生成单元测试

在 Agent 模式输入:

plain
为 user.service.ts 生成单元测试文件 src/modules/user/user.service.spec.ts。

重点测试:
1. create 方法:正常创建(验证密码被加密、返回值无 password)
2. create 方法:email 重复时抛出 ConflictException
3. findAll 方法:正常返回分页数据
4. findOne 方法:找不到用户时抛出 NotFoundException
5. remove 方法:验证执行的是软删除而不是硬删除

使用 Jest,Mock 掉 PrismaService,测试描述用中文

运行测试:

bash
npm test -- user.service.spec.ts --verbose

如果有测试失败,把错误信息贴到 Chat 里:

plain
测试运行报错:[粘贴错误信息],帮我修复

直到所有测试全绿为止。


五、常见问题处理

问题 原因 处理方式
npm run start:dev
报 TypeScript 编译错误
AI 生成的类型有误 把错误信息贴给 Chat,让 AI 修复
Prisma migration 失败 .env
里数据库连接串不对
检查 DATABASE_URL
格式和数据库是否启动
bcrypt 相关报错 缺少 @types/bcrypt npm install --save-dev @types/bcrypt
测试文件 Mock 报错 Mock 的方法路径不对 PrismaService 的 Mock 路径是 mockPrisma.user.findUnique
,不是 mockPrisma.findUnique
Swagger 页面打开是空的 main.ts
里没有配置 Swagger
手动加入第十步里的 Swagger 配置代码

Spec Coding 实战补充:06 用 Cursor Agent 从 0 搭 NestJS 项目

来源:Spec Coding实战/06 用 Cursor Agent 从 0 搭 NestJS 项目.md,已合并到本章节。

1. 项目目标

这一节做一个完整的用户管理模块,从 nest new 到接口可以跑通,全程用 Cursor Agent 驱动。

技术选型:

  • NestJS 10 + TypeScript
  • Prisma 5 + PostgreSQL
  • class-validator 参数校验
  • JWT 认证(仅搭结构,不展开认证逻辑)
  • Pino 日志

最终项目结构:

plain
my-project/
├── src/
│   ├── common/
│   │   ├── constants/
│   │   │   └── error-code.ts
│   │   ├── dto/
│   │   │   └── page-query.dto.ts
│   │   ├── exceptions/
│   │   │   └── business.exception.ts
│   │   ├── filters/
│   │   │   └── all-exceptions.filter.ts
│   │   └── interceptors/
│   │       └── transform.interceptor.ts
│   ├── config/
│   │   └── configuration.ts
│   ├── modules/
│   │   └── user/
│   │       ├── user.controller.ts
│   │       ├── user.service.ts
│   │       ├── user.module.ts
│   │       └── dto/
│   │           ├── create-user.dto.ts
│   │           ├── update-user.dto.ts
│   │           └── query-user.dto.ts
│   ├── prisma/
│   │   └── prisma.service.ts
│   └── app.module.ts
├── prisma/
│   └── schema.prisma
├── .cursorrules
├── .env
└── package.json

2. 第一步:初始化项目

创建 NestJS 项目

bash
npm i -g @nestjs/cli
nest new my-project --package-manager npm
cd my-project

安装依赖

bash
# Prisma
npm install prisma @prisma/client
npx prisma init

# 校验
npm install class-validator class-transformer

# 日志
npm install nestjs-pino pino-http pino-pretty

# 配置
npm install @nestjs/config

# JWT(后续认证用)
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt

# Swagger
npm install @nestjs/swagger swagger-ui-express

# 密码加密
npm install bcrypt
npm install -D @types/bcrypt

配置 .env

plain
# 数据库
DATABASE_URL="postgresql://postgres:password@localhost:5432/myproject_dev"

# JWT
JWT_SECRET="your-super-secret-key-change-in-production"
JWT_EXPIRES_IN="2h"

# 其他
NODE_ENV="development"
BCRYPT_ROUNDS=10

配置 .cursorrules

把第04节/第05节的 NestJS Rules 模板复制到项目根目录。


3. 第二步:搭建基础设施

这部分用 Cursor Agent 一次性生成,打开 Composer(Cmd+I),切换到 Agent 模式,输入:

plain
帮我搭建 NestJS 项目的基础设施层,需要创建以下内容:

1. src/prisma/prisma.service.ts
   - 继承 PrismaClient
   - 实现 onModuleInit 连接数据库
   - 实现 enableShutdownHooks

2. src/common/exceptions/business.exception.ts
   - 继承 HttpException
   - 接收 ErrorCode 枚举值
   - 包含 code、message 两个字段

3. src/common/constants/error-code.ts
   - ErrorCode 枚举:USER_NOT_FOUND(40401)、USER_ALREADY_EXISTS(40901)、
     INVALID_CREDENTIALS(40101)、FORBIDDEN(40301)、VALIDATION_ERROR(40001)

4. src/common/filters/all-exceptions.filter.ts
   - 捕获所有异常
   - BusinessException 返回对应 code 和 message
   - 其他异常返回 code:50000, message:'服务器内部错误'
   - 统一响应结构:{ code, message, data: null }

5. src/common/interceptors/transform.interceptor.ts
   - 包装成功响应:{ code: 0, message: 'ok', data: 原始返回值 }

6. src/common/dto/page-query.dto.ts
   - page: number(默认1,最小1)
   - pageSize: number(默认20,最大100)
   - sortBy: string(默认'createdAt')
   - order: 'asc' | 'desc'(默认'desc')

遵循 .cursorrules 的所有约定,完整 import,可直接运行。

Agent 生成完后,检查每个文件,确认 import 路径正确,没有语法错误。

生成的关键文件示例

business.exception.ts
typescript
import { HttpException, HttpStatus } from '@nestjs/common'
import { ErrorCode, ERROR_CODE_MAP } from '../constants/error-code'

export class BusinessException extends HttpException {
  readonly code: number
  readonly bizMessage: string

  constructor(errorCode: ErrorCode) {
    const { httpStatus, code, message } = ERROR_CODE_MAP[errorCode]
    super({ code, message, data: null }, httpStatus)
    this.code = code
    this.bizMessage = message
  }
}
error-code.ts
typescript
import { HttpStatus } from '@nestjs/common'

export enum ErrorCode {
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
  INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
  FORBIDDEN = 'FORBIDDEN',
  VALIDATION_ERROR = 'VALIDATION_ERROR',
}

export const ERROR_CODE_MAP: Record<
  ErrorCode,
  { httpStatus: HttpStatus; code: number; message: string }
> = {
  [ErrorCode.USER_NOT_FOUND]: {
    httpStatus: HttpStatus.NOT_FOUND,
    code: 40401,
    message: '用户不存在',
  },
  [ErrorCode.USER_ALREADY_EXISTS]: {
    httpStatus: HttpStatus.CONFLICT,
    code: 40901,
    message: '邮箱已被注册',
  },
  [ErrorCode.INVALID_CREDENTIALS]: {
    httpStatus: HttpStatus.UNAUTHORIZED,
    code: 40101,
    message: '邮箱或密码错误',
  },
  [ErrorCode.FORBIDDEN]: {
    httpStatus: HttpStatus.FORBIDDEN,
    code: 40301,
    message: '无权限执行此操作',
  },
  [ErrorCode.VALIDATION_ERROR]: {
    httpStatus: HttpStatus.BAD_REQUEST,
    code: 40001,
    message: '请求参数校验失败',
  },
}
transform.interceptor.ts
typescript
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, any> {
  intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        code: 0,
        message: 'ok',
        data: data ?? null,
      })),
    )
  }
}

4. 第三步:配置 Prisma Schema

编辑 prisma/schema.prisma

plain
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String     @id @default(uuid())
  email     String     @unique
  password  String
  name      String
  status    UserStatus @default(ACTIVE)
  isDeleted Boolean    @default(false) @map("is_deleted")
  createdAt DateTime   @default(now()) @map("created_at")
  updatedAt DateTime   @updatedAt @map("updated_at")

  @@index([email])
  @@index([createdAt])
  @@index([isDeleted, status])
  @@map("users")
}

enum UserStatus {
  ACTIVE
  DISABLED
}

执行迁移:

bash
npx prisma migrate dev --name init_user_table
npx prisma generate

5. 第四步:生成用户模块

打开 Composer,继续用 Agent:

plain
按照以下规格,在 src/modules/user/ 下创建完整的用户模块:

**DTO:**
create-user.dto.ts:
- email: string(@IsEmail,@IsNotEmpty)
- password: string(@IsString,@MinLength(8),@MaxLength(32))
- name: string(@IsString,@MinLength(2),@MaxLength(50))

update-user.dto.ts:
- 继承 PartialType(CreateUserDto),password 字段用 @IsOptional

query-user.dto.ts:
- 继承 PageQueryDto
- search?: string(@IsOptional,@IsString,模糊搜索 name 和 email)
- status?: UserStatus(@IsOptional,@IsEnum(UserStatus))

**UserService 方法:**
- create(dto): 检查邮箱重复 → bcrypt 加密密码 → 创建用户 → 返回(omit password)
- findAll(query): 支持 search 模糊搜索 + status 过滤 + 分页 + 排序,返回 { list, total, page, pageSize }
- findOne(id): 查询单个用户,不存在抛 USER_NOT_FOUND
- update(id, dto): 更新用户信息,id 不存在抛 USER_NOT_FOUND
- remove(id): 软删除(isDeleted = true)

**UserController:**
- POST /users → create
- GET /users → findAll(需要加 @Query() 参数)
- GET /users/:id → findOne
- PATCH /users/:id → update
- DELETE /users/:id → remove
- 所有方法加 @ApiOperation Swagger 注释

使用 PrismaService,遵循 .cursorrules,完整 import,可直接运行。
引用 @src/common/ 目录下的基础设施。

生成后的关键文件

user.service.ts
typescript
import { Injectable } from '@nestjs/common'
import * as bcrypt from 'bcrypt'
import { PrismaService } from '../../prisma/prisma.service'
import { BusinessException } from '../../common/exceptions/business.exception'
import { ErrorCode } from '../../common/constants/error-code'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { QueryUserDto } from './dto/query-user.dto'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class UserService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly configService: ConfigService,
  ) {}

  async create(dto: CreateUserDto) {
    const existing = await this.prisma.user.findUnique({
      where: { email: dto.email },
    })
    if (existing) {
      throw new BusinessException(ErrorCode.USER_ALREADY_EXISTS)
    }

    const rounds = this.configService.get<number>('BCRYPT_ROUNDS', 10)
    const hashedPassword = await bcrypt.hash(dto.password, rounds)

    const user = await this.prisma.user.create({
      data: {
        email: dto.email,
        password: hashedPassword,
        name: dto.name,
      },
      omit: { password: true },
    })

    return user
  }

  async findAll(query: QueryUserDto) {
    const { page = 1, pageSize = 20, sortBy = 'createdAt', order = 'desc', search, status } = query

    const whereCondition: any = {
      isDeleted: false,
      ...(status && { status }),
      ...(search && {
        OR: [
          { name: { contains: search, mode: 'insensitive' } },
          { email: { contains: search, mode: 'insensitive' } },
        ],
      }),
    }

    const [list, total] = await this.prisma.$transaction([
      this.prisma.user.findMany({
        where: whereCondition,
        skip: (page - 1) * pageSize,
        take: pageSize,
        orderBy: { [sortBy]: order },
        omit: { password: true },
      }),
      this.prisma.user.count({ where: whereCondition }),
    ])

    return { list, total, page, pageSize }
  }

  async findOne(id: string) {
    const user = await this.prisma.user.findFirst({
      where: { id, isDeleted: false },
      omit: { password: true },
    })
    if (!user) {
      throw new BusinessException(ErrorCode.USER_NOT_FOUND)
    }
    return user
  }

  async update(id: string, dto: UpdateUserDto) {
    await this.findOne(id) // 验证存在

    if (dto.password) {
      const rounds = this.configService.get<number>('BCRYPT_ROUNDS', 10)
      dto.password = await bcrypt.hash(dto.password, rounds)
    }

    return this.prisma.user.update({
      where: { id },
      data: dto,
      omit: { password: true },
    })
  }

  async remove(id: string) {
    await this.findOne(id) // 验证存在
    await this.prisma.user.update({
      where: { id },
      data: { isDeleted: true },
    })
    return null
  }
}

6. 第五步:注册模块,配置全局

app.module.ts

typescript
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import { LoggerModule } from 'nestjs-pino'
import { PrismaModule } from './prisma/prisma.module'
import { UserModule } from './modules/user/user.module'
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'
import { TransformInterceptor } from './common/interceptors/transform.interceptor'
import configuration from './config/configuration'

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
    LoggerModule.forRoot({
      pinoHttp: {
        transport:
          process.env.NODE_ENV !== 'production'
            ? { target: 'pino-pretty', options: { colorize: true } }
            : undefined,
      },
    }),
    PrismaModule,
    UserModule,
  ],
  providers: [
    { provide: APP_FILTER, useClass: AllExceptionsFilter },
    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
        transform: true,
        forbidNonWhitelisted: true,
      }),
    },
  ],
})
export class AppModule {}

7. 第六步:启动验证

bash
npm run start:dev

正常启动后,访问 Swagger 文档:http://localhost:3000/api

用 Postman 或 curl 测试:

bash
# 创建用户
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"Test1234","name":"张三"}'

# 预期响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "uuid...",
    "email": "test@example.com",
    "name": "张三",
    "status": "ACTIVE",
    "createdAt": "2025-01-01T00:00:00.000Z"
  }
}

# 查询列表
curl "http://localhost:3000/users?page=1&pageSize=10&search=张"

# 邮箱重复测试
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"Test1234","name":"李四"}'

# 预期响应
{
  "code": 40901,
  "message": "邮箱已被注册",
  "data": null
}

8. 小结:Agent 驱动开发的节奏

用 Cursor Agent 从 0 搭项目,有几个关键节奏:

分层推进,不要一次要太多

  • 先搭基础设施(filter、interceptor、exception)
  • 再建数据模型(prisma schema + migrate)
  • 最后做业务模块(dto + service + controller)

每一层验证通过再进入下一层,出问题好定位。

给 Agent 的任务要具体 任务里写清楚:方法名、参数、返回值、异常场景。越模糊的任务,生成质量越差。

生成完立刻编译验证
bash
npm run build

有类型错误或编译错误,立刻贴回 Chat 修。不要攒一堆问题最后一起处理。