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 Schema、API 契约、请求类型、响应类型
交互策略:
- 先给 AI 看类型定义文件
- 告诉 AI:"基于这个类型定义,帮我实现 API"
- AI 生成的代码会自动符合契约
示例 Prompt:
"基于 types/user.ts 中的 CreateUserRequest 和 UserResponse 类型,
帮我实现 POST /api/users 接口,使用 Prisma 操作数据库"本节小结
| 原则 | 说明 |
|---|---|
| 类型先行 | 写代码前先定义 TypeScript 类型 |
| 单一数据源 | 类型定义集中管理,前后端共用 |
| 输入输出分离 | CreateInput 和 Entity 是不同的类型 |
| 运行时验证 | 用 Zod 在运行时验证数据 |
