⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

2.4.1 先商量好再动手——契约先行

一句话破题

契约先行的本质是:在写任何代码之前,先用 TypeScript 类型把接口格式定死。前后端基于同一份类型定义开发,联调时只需验证实现是否符合契约。

契约的形式

TypeScript 类型定义

typescript
// types/api.ts
// 这就是"契约":请求和响应的格式

// 通用响应格式
interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

// 用户相关
interface User {
  id: string
  name: string
  email: string
  avatar?: string
  createdAt: string
}

interface CreateUserInput {
  name: string
  email: string
  password: string
}

interface UpdateUserInput {
  name?: string
  avatar?: string
}

// 文章相关
interface Post {
  id: string
  title: string
  content: string
  author: User
  createdAt: string
  updatedAt: string
}

interface CreatePostInput {
  title: string
  content: string
}

Zod Schema(运行时验证)

typescript
// schemas/user.ts
import { z } from 'zod'

export const CreateUserSchema = z.object({
  name: z.string().min(2, '姓名至少 2 个字符'),
  email: z.string().email('邮箱格式不正确'),
  password: z.string().min(8, '密码至少 8 位'),
})

export const UpdateUserSchema = z.object({
  name: z.string().min(2).optional(),
  avatar: z.string().url().optional(),
})

// 从 Zod Schema 导出 TypeScript 类型
export type CreateUserInput = z.infer<typeof CreateUserSchema>
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>

契约的组织方式

目录结构

src/
├── types/
│   ├── api.ts        # 通用 API 类型
│   ├── user.ts       # 用户相关类型
│   └── post.ts       # 文章相关类型
├── schemas/
│   ├── user.ts       # 用户验证 Schema
│   └── post.ts       # 文章验证 Schema
└── app/
    └── api/
        └── users/
            └── route.ts  # API 实现

完整的契约文件示例

typescript
// types/user.ts

// ============ 实体类型 ============
export interface User {
  id: string
  name: string
  email: string
  avatar: string | null
  role: 'admin' | 'user'
  createdAt: string
  updatedAt: string
}

// ============ 请求类型 ============
export interface CreateUserRequest {
  name: string
  email: string
  password: string
}

export interface UpdateUserRequest {
  name?: string
  avatar?: string
}

export interface ListUsersRequest {
  page?: number
  pageSize?: number
  search?: string
}

// ============ 响应类型 ============
export interface UserResponse {
  code: number
  message: string
  data: User
}

export interface UsersListResponse {
  code: number
  message: string
  data: {
    users: User[]
    total: number
    page: number
    pageSize: number
  }
}

前后端如何使用契约

后端实现

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { CreateUserSchema } from '@/schemas/user'
import type { UserResponse, CreateUserRequest } from '@/types/user'

export async function POST(request: NextRequest) {
  const body: CreateUserRequest = await request.json()
  
  // 使用 Zod 验证
  const validated = CreateUserSchema.safeParse(body)
  if (!validated.success) {
    return NextResponse.json({
      code: 400,
      message: '参数错误',
      errors: validated.error.flatten().fieldErrors,
    }, { status: 400 })
  }
  
  // 创建用户
  const user = await prisma.user.create({
    data: validated.data,
  })
  
  // 返回符合契约的响应
  const response: UserResponse = {
    code: 200,
    message: '创建成功',
    data: user,
  }
  
  return NextResponse.json(response)
}

前端调用

typescript
// services/user.ts
import type { 
  CreateUserRequest, 
  UserResponse,
  UsersListResponse 
} from '@/types/user'

export async function createUser(data: CreateUserRequest): Promise<UserResponse> {
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
  return res.json()
}

export async function getUsers(params?: { page?: number }): Promise<UsersListResponse> {
  const searchParams = new URLSearchParams()
  if (params?.page) searchParams.set('page', String(params.page))
  
  const res = await fetch(`/api/users?${searchParams}`)
  return res.json()
}

API 设计规范

RESTful 风格

操作HTTP 方法URL示例
列表GET/api/users获取用户列表
详情GET/api/users/:id获取单个用户
创建POST/api/users创建用户
更新PUT/PATCH/api/users/:id更新用户
删除DELETE/api/users/:id删除用户

响应格式统一

typescript
// 成功响应
{
  "code": 200,
  "message": "操作成功",
  "data": { ... }
}

// 错误响应
{
  "code": 400,
  "message": "参数错误",
  "errors": {
    "email": ["邮箱格式不正确"]
  }
}

// 分页响应
{
  "code": 200,
  "message": "获取成功",
  "data": {
    "items": [...],
    "total": 100,
    "page": 1,
    "pageSize": 10
  }
}

觉知:契约设计的常见问题

1. 类型过于宽泛

typescript
// ❌ 太宽泛,AI 不知道具体格式
interface ApiResponse {
  data: any
}

// ✅ 明确的类型定义
interface UserResponse {
  code: number
  message: string
  data: User
}

2. 请求和响应类型混用

typescript
// ❌ 创建时不应该有 id
interface User {
  id: string
  name: string
}
// 创建用户时也要传 id?

// ✅ 分离输入和输出类型
interface User {
  id: string
  name: string
}

interface CreateUserInput {
  name: string  // 创建时不需要 id
}

3. 可选字段滥用

typescript
// ❌ 全部可选,契约形同虚设
interface CreateUserInput {
  name?: string
  email?: string
}

// ✅ 必填和可选分明
interface CreateUserInput {
  name: string   // 必填
  email: string  // 必填
  avatar?: string  // 可选
}

AI 协作指南

核心意图:让 AI 基于契约生成代码

关键术语TypeScript 类型Zod SchemaAPI 契约请求类型响应类型

交互策略

  1. 先给 AI 看类型定义文件
  2. 告诉 AI:"基于这个类型定义,帮我实现 API"
  3. AI 生成的代码会自动符合契约
示例 Prompt:
"基于 types/user.ts 中的 CreateUserRequest 和 UserResponse 类型,
帮我实现 POST /api/users 接口,使用 Prisma 操作数据库"

本节小结

原则说明
类型先行写代码前先定义 TypeScript 类型
单一数据源类型定义集中管理,前后端共用
输入输出分离CreateInput 和 Entity 是不同的类型
运行时验证用 Zod 在运行时验证数据