Skip to content

4.3 创建你的第一个 Spec

🎯 本章目标

通过 3 个递进式实战案例,你将学会:

  • ✅ 从零开始使用 Spec-kit 创建项目
  • ✅ 编写高质量的规格说明书(spec.md)
  • ✅ 生成技术方案(plan.md)和任务清单(tasks.md)
  • ✅ 使用 AI 自动实现代码

学习路径:

案例 1(10 分钟) → 个人作品集(静态网站)
   基础:体验完整流程

案例 2(30 分钟) → 相册管理器(前端应用)
   进阶:澄清流程 + 质量验证

案例 3(60 分钟) → 待办事项 API(全栈应用)
   高级:契约优先 + 前后端协同

📘 案例 1:个人作品集网站(10 分钟)

项目背景

需求: 创建一个简单的个人作品集网站,展示你的项目、技能和联系方式。

技术约束:

  • 静态网站(无服务器)
  • 不使用前端框架(纯 HTML/CSS/JavaScript)
  • 最小依赖

Step 1: 安装 Spec-kit

bash
# 方式 1:永久安装(推荐)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git

# 验证安装
specify --version

# 方式 2:一次性使用(无需安装)
uvx --from git+https://github.com/github/spec-kit.git specify init portfolio

Step 2: 初始化项目

bash
# 创建项目
specify init portfolio --ai claude

# 进入项目目录
cd portfolio

生成的结构:

portfolio/
├── .claude/
│   └── commands/
│       ├── speckit.constitution.md
│       ├── speckit.specify.md
│       ├── speckit.plan.md
│       ├── speckit.tasks.md
│       └── ...
├── .specify/
│   ├── memory/
│   ├── scripts/
│   └── templates/
├── specs/                    # 即将创建
└── .gitignore

Step 3: 创建宪法(可选但推荐)

在 Claude Code 中输入:

/speckit.constitution

AI 会问你一些问题:

Q: 这个项目有什么不可协商的原则吗?

A: 静态网站,不用服务器,只用 HTML/CSS/JavaScript,不用任何前端框架

生成的宪法(.specify/memory/constitution.md):

markdown
# Portfolio Constitution v1.0.0

## Article I: Static-First Architecture
所有功能必须在浏览器端实现,禁止服务器端处理。

Rationale: 简化部署,降低成本,提升性能
Compliance: 所有代码必须是客户端 JavaScript

## Article II: Zero Framework Dependency
禁止使用前端框架(React, Vue, Angular 等)

Rationale: 长期可维护性,快速加载
Compliance: 仅使用原生 Web APIs

## Article III: Minimal Dependencies
外部依赖不超过 1 个(仅允许构建工具)

Rationale: 减少维护负担,提升安全性
Compliance: package.json 中 dependencies 字段最多 1 项

## Constraints
- Performance: First Contentful Paint < 1 second
- Compatibility: 支持现代浏览器(Chrome 90+, Firefox 88+, Safari 14+)
- Deployment: 可部署到 GitHub Pages / Netlify / Vercel

Step 4: 编写规格说明

bash
/speckit.specify 个人作品集网站,包含首页、项目展示、技能列表、联系方式

AI 生成的 specs/001-portfolio/spec.md

markdown
# Feature: Personal Portfolio Website

**Feature Branch**: 001-portfolio
**Created**: 2025-01-15
**Status**: Draft

## User Scenarios

### P1: Homepage with Introduction
**Description**: 访客打开网站时看到个人介绍、照片和快速导航

**Priority**: P1 (Critical)
**Priority Justification**: 首页是访客的第一印象,决定是否继续浏览

**Independent Test**: 访问网站根目录,验证页面加载且显示完整内容

**Acceptance Criteria**:
- Given 访客访问网站根 URL
- When 页面加载完成
- Then 显示个人照片、姓名、职位和简介(< 200 字)
- And 显示导航菜单(项目、技能、联系)
- And First Contentful Paint < 1 second

### P2: Projects Showcase
**Description**: 展示代表性项目,包含描述、技术栈和链接

**Priority**: P2 (Important)
**Priority Justification**: 项目展示是作品集的核心价值

**Acceptance Criteria**:
- Given 访客点击"项目"导航
- When 项目页面加载
- Then 显示至少 3 个项目
- And 每个项目显示:标题、描述、技术栈标签、链接、截图
- And 点击链接跳转到项目详情或 GitHub

### P3: Skills List
**Description**: 分类展示技能(编程语言、框架、工具)

**Priority**: P3 (Nice-to-have)

**Acceptance Criteria**:
- Given 访客点击"技能"导航
- When 技能页面加载
- Then 显示技能分类(前端、后端、工具)
- And 每个技能显示熟练度(初级/中级/高级)

### P4: Contact Form
**Description**: 访客可以通过表单发送消息

**Priority**: P3 (Nice-to-have)

**Acceptance Criteria**:
- Given 访客在联系页面
- When 填写姓名、邮箱、消息并提交
- Then 显示提交成功信息
- And NEEDS CLARIFICATION: 消息如何发送?(无服务器约束)

## Requirements

### Functional Requirements
- FR-001: 网站 MUST 加载所有内容在 3 秒内(3G 网络)
- FR-002: 网站 MUST 响应式设计(移动端、平板、桌面)
- FR-003: 项目 MUST 显示技术栈标签
- FR-004: NEEDS CLARIFICATION: 联系表单消息如何处理?

### Key Entities
- **Project**:
  - title: 项目标题
  - description: 简短描述(< 150 字)
  - tech_stack: 技术栈列表
  - links: {github: URL, demo: URL}
  - image: 截图 URL

- **Skill**:
  - name: 技能名称
  - category: 分类(frontend/backend/tools)
  - proficiency: 熟练度(beginner/intermediate/advanced)

## Success Criteria
- SC-001: 页面加载 First Contentful Paint < 1 second
- SC-002: 所有图片优化后 < 200KB
- SC-003: Lighthouse Performance Score > 90
- SC-004: 移动端可用性得分 > 95

## Edge Cases
### Slow Network
- Scenario: 用户在慢速网络环境
- Expected Behavior: 显示加载进度指示器,图片懒加载

### Missing Data
- Scenario: 某些项目缺少截图
- Expected Behavior: 显示占位图(placeholder)

### Browser Compatibility
- Scenario: 用户使用旧浏览器
- Expected Behavior: 显示"请升级浏览器"提示

Step 5: 澄清阶段(解决 NEEDS CLARIFICATION)

bash
/speckit.clarify

AI 提问:

Question 1 of 2: Contact Form Message Handling

由于项目必须是静态网站(无服务器),联系表单消息如何处理?

Options:
A) 使用第三方服务(Formspree, Netlify Forms)
B) 仅显示邮箱地址,无实际表单
C) 使用 mailto: 链接打开邮件客户端

Recommended: A(第三方服务)- 用户体验最佳,符合静态架构

Your choice: [A]

AI 更新 spec.md:

markdown
- FR-004: 联系表单 MUST 使用 Formspree 发送消息(免费 tier)
- Added Edge Case: Formspree 配额用完 → 显示降级到 mailto: 链接

Step 6: 技术方案

bash
/speckit.plan Vite 作为构建工具,纯 HTML/CSS/JavaScript,Formspree 处理表单

生成 specs/001-portfolio/plan.md(节选):

markdown
# Implementation Plan: Personal Portfolio Website

## Technical Context
- **Language**: HTML5, CSS3, JavaScript (ES2020+)
- **Build Tool**: Vite 5.0+(仅此一个依赖)
- **Styling**: CSS Grid + Flexbox(无 CSS 框架)
- **Form Handling**: Formspree(免费 tier)
- **Deployment**: GitHub Pages
- **Performance Goals**: FCP < 1s, Lighthouse > 90

## Constitution Check
✅ Article I: Static-First(所有逻辑在客户端)
✅ Article II: Zero Framework(无 React/Vue)
✅ Article III: Minimal Dependencies(仅 Vite)

## Project Structure

portfolio/ ├── index.html ├── projects.html ├── skills.html ├── contact.html ├── css/ │ ├── style.css │ └── responsive.css ├── js/ │ ├── main.js │ └── form.js └── images/ ├── profile.jpg └── projects/


## Data Model
```javascript
// projects.js(静态数据)
const projects = [
  {
    id: 1,
    title: "E-commerce Platform",
    description: "Full-stack online store with payment integration",
    tech_stack: ["React", "Node.js", "PostgreSQL"],
    links: {
      github: "https://github.com/user/ecommerce",
      demo: "https://demo.example.com"
    },
    image: "images/projects/ecommerce.png"
  },
  // ... more projects
];

API Contract

N/A(静态网站,无后端 API)

External Service Integration

javascript
// Formspree Integration
// Form action: https://formspree.io/f/{YOUR_FORM_ID}

<form action="https://formspree.io/f/abc123" method="POST">
  <input type="text" name="name" required>
  <input type="email" name="_replyto" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

### Step 7: 任务分解

```bash
/speckit.tasks

生成 specs/001-portfolio/tasks.md(节选):

markdown
# Tasks: Personal Portfolio Website

## Phase 1: Setup
- [ ] T001 Initialize Vite project at /absolute/path/portfolio/
  Command: `npm create vite@latest . -- --template vanilla`

- [ ] T002 [P] Configure project structure at /absolute/path/portfolio/
  Create directories: css/, js/, images/, images/projects/

- [ ] T003 [P] Add .gitignore at /absolute/path/portfolio/.gitignore
  Ignore: node_modules/, dist/, .DS_Store

## Phase 2: Foundation
- [ ] T004 Create base HTML template at /absolute/path/portfolio/index.html
  Include: meta tags, viewport, semantic HTML5

- [ ] T005 Implement CSS Grid layout at /absolute/path/portfolio/css/style.css
  Sections: header, nav, main, footer

- [ ] T006 Add responsive styles at /absolute/path/portfolio/css/responsive.css
  Breakpoints: 768px (tablet), 480px (mobile)

## Phase 3: User Stories

### US1: Homepage with Introduction
- [ ] T007 [US1] Create hero section at /absolute/path/portfolio/index.html
  Content: Profile image, name, title, bio (< 200 words)

- [ ] T008 [P] [US1] Style hero section at /absolute/path/portfolio/css/style.css
  Layout: Centered, flexbox, profile image circle (200px)

- [ ] T009 [P] [US1] Add navigation at /absolute/path/portfolio/index.html
  Links: Projects, Skills, Contact

**CHECKPOINT**: Verify homepage loads < 1s, responsive on mobile

### US2: Projects Showcase
- [ ] T010 [US2] Create projects data at /absolute/path/portfolio/js/projects.js
  Data: Array of 3+ projects with all required fields

- [ ] T011 [US2] Build projects page at /absolute/path/portfolio/projects.html
  Layout: Grid of project cards

- [ ] T012 [P] [US2] Implement card component at /absolute/path/portfolio/js/main.js
  Function: renderProjectCard(project) → HTML string

- [ ] T013 [US2] Add lazy loading for images at /absolute/path/portfolio/js/main.js
  Use: Intersection Observer API

**CHECKPOINT**: Verify all projects display, images lazy load

### US3: Skills List
- [ ] T014 [US3] Create skills page at /absolute/path/portfolio/skills.html
  Sections: Frontend, Backend, Tools

- [ ] T015 [P] [US3] Style skill tags at /absolute/path/portfolio/css/style.css
  Design: Colored tags with proficiency indicator

### US4: Contact Form
- [ ] T016 [US4] Integrate Formspree at /absolute/path/portfolio/contact.html
  Action: https://formspree.io/f/{YOUR_ID}

- [ ] T017 [P] [US4] Add form validation at /absolute/path/portfolio/js/form.js
  Validate: Email format, required fields

- [ ] T018 [US4] Implement success message at /absolute/path/portfolio/js/form.js
  Display: "Message sent!" modal

## Phase 4: Polish
- [ ] T019 Optimize images at /absolute/path/portfolio/images/
  Tools: ImageOptim, convert to WebP

- [ ] T020 Run Lighthouse audit
  Target: Performance > 90, Accessibility > 95

- [ ] T021 Add meta tags for SEO at /absolute/path/portfolio/index.html
  Include: description, keywords, og:image

- [ ] T022 Test on mobile devices
  Browsers: iOS Safari, Android Chrome

Step 8: 实现

bash
/speckit.implement

AI 会按照 tasks.md 顺序自动生成代码:

  1. 初始化项目(T001-T003)
  2. 创建基础布局(T004-T006)
  3. 实现首页(T007-T009)
  4. 实现项目展示(T010-T013)
  5. 实现技能列表(T014-T015)
  6. 实现联系表单(T016-T018)
  7. 优化和打磨(T019-T022)

最终输出:

portfolio/
├── index.html(首页,175 行)
├── projects.html(项目页,120 行)
├── skills.html(技能页,95 行)
├── contact.html(联系页,88 行)
├── css/
│   ├── style.css(主样式,230 行)
│   └── responsive.css(响应式,85 行)
├── js/
│   ├── main.js(主逻辑,150 行)
│   ├── projects.js(数据,60 行)
│   └── form.js(表单验证,45 行)
└── images/
    ├── profile.jpg
    └── projects/
        ├── project1.png
        ├── project2.png
        └── project3.png

Step 9: 验证

bash
# 本地运行
npm run dev

# 打开浏览器
# http://localhost:5173

# 运行 Lighthouse 审计
# Chrome DevTools → Lighthouse → Generate Report

# 验证结果:
# ✅ Performance: 95
# ✅ Accessibility: 98
# ✅ Best Practices: 100
# ✅ SEO: 92

Step 10: 部署

bash
# 构建生产版本
npm run build

# 部署到 GitHub Pages
git add .
git commit -m "Initial portfolio implementation"
git push origin main

# 在 GitHub 仓库设置中启用 GitHub Pages
# Settings → Pages → Source: main branch / docs folder

📙 案例 2:相册管理器(30 分钟)

项目背景

需求: 创建一个相册管理器,用户可以创建相册、上传照片、查看照片

技术要求:

  • 前端:React 18 + TypeScript
  • 存储:浏览器 IndexedDB
  • 构建:Vite

完整流程(简化版,重点突出差异)

Step 1-2: 初始化(同案例 1)

bash
specify init photo-album --ai claude
cd photo-album

Step 3: 宪法

bash
/speckit.constitution
markdown
# Photo Album Constitution v1.0

## Article I: Offline-First Architecture
所有功能必须离线可用(使用 IndexedDB)

## Article II: TypeScript Mandatory
所有代码必须使用 TypeScript,类型覆盖率 > 95%

## Article III: Component-Driven UI
UI 必须基于可复用组件,每个组件有单元测试

## Constraints
- Storage: 最大 50MB IndexedDB quota
- Performance: 相册加载 < 500ms
- UX: 支持拖拽上传

Step 4: 规格

bash
/speckit.specify 相册管理器,用户可以创建相册、上传照片、查看缩略图、删除照片

生成的 spec.md(关键部分):

markdown
## User Scenarios

### P1: Create Album
**Priority**: P1
**Acceptance Criteria**:
- Given 用户在主页
- When 点击"创建相册"
- Then 弹出表单输入相册名称
- And 保存后相册显示在列表中
- And 相册创建耗时 < 200ms

### P2: Upload Photos
**Priority**: P1
**Acceptance Criteria**:
- Given 用户在相册详情页
- When 拖拽照片文件到上传区
- Then 显示上传进度(每张照片)
- And 上传完成后显示缩略图(150x150px)
- And 支持 JPG, PNG, GIF(最大 5MB/张)

### P3: View Photos
**Priority**: P2
**Acceptance Criteria**:
- Given 用户在相册详情页
- When 点击照片缩略图
- Then 在模态框中显示原图
- And 支持左右箭头切换照片
- And 显示照片元数据(文件名、大小、上传时间)

### NEEDS CLARIFICATION
- 删除照片是否需要二次确认?
- 相册数量有限制吗?

Step 5: 澄清

bash
/speckit.clarify

Q1: 删除照片确认

选项:
A) 需要二次确认(防误删)
B) 直接删除(快速操作)
C) 软删除 + 30 天恢复期

推荐:A

你的选择:A

更新:
- FR-015: 删除照片必须弹出确认对话框
- Edge Case: 误删 → 显示 Undo 提示(5 秒)

Q2: 相册数量限制

推荐:50 个相册(避免 IndexedDB 过大)

你的选择:50

更新:
- FR-016: 相册数量限制 50 个
- Edge Case: 达到上限 → 显示"请删除旧相册"提示

Step 6: 技术方案

bash
/speckit.plan React 18 + TypeScript + Vite,用 Dexie.js 封装 IndexedDB

生成的 plan.md(关键部分):

markdown
## Technical Context
- Language: TypeScript 5.0+
- Framework: React 18.2+
- State Management: Zustand 4.0
- Database: Dexie.js (IndexedDB wrapper)
- Build Tool: Vite 5.0
- Testing: Vitest + React Testing Library

## Data Model

```typescript
// src/types/index.ts
export interface Album {
  id: string;  // UUID
  name: string;
  created_at: number;  // Unix timestamp
  photo_count: number;
}

export interface Photo {
  id: string;
  album_id: string;
  filename: string;
  data: Blob;  // 原图
  thumbnail: Blob;  // 缩略图 150x150
  size: number;  // bytes
  uploaded_at: number;
}

Database Schema (Dexie)

typescript
// src/db.ts
import Dexie, { Table } from 'dexie';

export class PhotoAlbumDB extends Dexie {
  albums!: Table<Album>;
  photos!: Table<Photo>;

  constructor() {
    super('PhotoAlbumDB');
    this.version(1).stores({
      albums: 'id, name, created_at',
      photos: 'id, album_id, uploaded_at'
    });
  }
}

export const db = new PhotoAlbumDB();

Component Structure

src/
├── components/
│   ├── AlbumList.tsx(相册列表)
│   ├── AlbumCard.tsx(单个相册卡片)
│   ├── PhotoUploader.tsx(照片上传组件)
│   ├── PhotoGrid.tsx(照片网格)
│   ├── PhotoModal.tsx(照片查看器)
│   └── DeleteConfirmDialog.tsx(删除确认)
├── hooks/
│   ├── useAlbums.ts(相册操作)
│   └── usePhotos.ts(照片操作)
├── services/
│   ├── albumService.ts(相册 CRUD)
│   └── photoService.ts(照片 CRUD + 缩略图生成)
├── types/
│   └── index.ts(类型定义)
└── db.ts(Dexie 数据库)

Constitution Check

✅ Article I: Offline-First(IndexedDB) ✅ Article II: TypeScript(所有文件 .ts/.tsx) ✅ Article III: Component-Driven(12 个可复用组件)


#### Step 7: 任务分解

```bash
/speckit.tasks

tasks.md(节选):

markdown
## Phase 1: Setup
- [ ] T001 Initialize Vite + React + TypeScript project
- [ ] T002 [P] Install dependencies (Dexie, Zustand, React Testing Library)
- [ ] T003 [P] Configure TypeScript strict mode

## Phase 2: Foundation
- [ ] T004 Create Dexie database at /absolute/path/src/db.ts
- [ ] T005 Implement Album service at /absolute/path/src/services/albumService.ts
  Functions:
    - createAlbum(name: string): Promise<string>
    - getAlbums(): Promise<Album[]>
    - deleteAlbum(id: string): Promise<void>

- [ ] T006 Implement Photo service at /absolute/path/src/services/photoService.ts
  Functions:
    - uploadPhoto(albumId, file: File): Promise<string>
    - generateThumbnail(file: File): Promise<Blob>
    - getPhotos(albumId): Promise<Photo[]>
    - deletePhoto(id: string): Promise<void>

## Phase 3: User Stories

### US1: Create Album
- [ ] T007 [US1] Create AlbumList component at /absolute/path/src/components/AlbumList.tsx
  Props: none
  State: albums from useAlbums hook
  Render: Grid of AlbumCard components + "Create" button

- [ ] T008 [P] [US1] Create AlbumCard component at /absolute/path/src/components/AlbumCard.tsx
  Props: album: Album, onDelete: () => void
  Render: Album name, photo count, delete button

- [ ] T009 [US1] Implement useAlbums hook at /absolute/path/src/hooks/useAlbums.ts
  State management: Zustand store
  Methods: createAlbum, loadAlbums, deleteAlbum

- [ ] T010 [P] [US1] Write tests at /absolute/path/src/components/AlbumList.test.tsx
  Test cases:
    - renders empty state
    - renders album list
    - creates album on button click
    - deletes album with confirmation

**CHECKPOINT**: Create album, verify persists after refresh

### US2: Upload Photos (Drag & Drop)
- [ ] T011 [US2] Create PhotoUploader at /absolute/path/src/components/PhotoUploader.tsx
  Features:
    - Drag & drop zone
    - File input fallback
    - Upload progress per file
    - Validation (file type, size)

- [ ] T012 [P] [US2] Implement thumbnail generation at /absolute/path/src/services/photoService.ts
  Function: generateThumbnail(file): Promise<Blob>
  Logic:
    1. Create canvas 150x150
    2. Draw image with aspect ratio preserved
    3. Convert to Blob (JPEG, quality 0.8)

- [ ] T013 [US2] Create PhotoGrid at /absolute/path/src/components/PhotoGrid.tsx
  Props: photos: Photo[]
  Render: Grid of thumbnails (CSS Grid, 4 columns)

**CHECKPOINT**: Upload photos, verify thumbnails generated

### US3: View Photos
- [ ] T014 [US3] Create PhotoModal at /absolute/path/src/components/PhotoModal.tsx
  Props: photo: Photo, onClose, onNext, onPrev
  Features:
    - Full-size image
    - Left/right arrows
    - Metadata display
    - Keyboard navigation (←/→/Esc)

## Phase 4: Polish
- [ ] T015 Add TypeScript coverage check
  Target: > 95% (no `any` types)

- [ ] T016 Implement error handling
  - IndexedDB quota exceeded → show warning
  - File upload fails → retry logic

- [ ] T017 Performance optimization
  - Lazy load photos (virtualization)
  - Debounce search input
  - Memoize thumbnail rendering

Step 8: 实现

bash
/speckit.implement

AI 按照 tasks 顺序生成所有代码(约 1,200 行 TypeScript/TSX)

Step 9: 验证

bash
# 运行开发服务器
npm run dev

# 测试流程:
# 1. 创建相册 "Vacation 2025"
# 2. 拖拽 5 张照片上传
# 3. 验证缩略图生成
# 4. 点击照片查看大图
# 5. 删除 1 张照片(验证确认对话框)
# 6. 刷新页面(验证数据持久化)

# 运行测试
npm run test

# TypeScript 类型检查
npm run type-check

# 结果:
# ✅ 所有测试通过(32 tests)
# ✅ TypeScript 覆盖率 98%
# ✅ IndexedDB 数据持久化

📕 案例 3:待办事项 API(60 分钟)

项目背景

需求: 创建一个 RESTful API 用于管理待办事项,支持 CRUD 操作、优先级排序、标签过滤

技术栈:

  • 后端:Python FastAPI
  • 数据库:PostgreSQL
  • 认证:JWT
  • 测试:pytest

重点学习: 契约优先开发(Contract-First Development)

完整流程(简化,突出契约优先)

Step 1-3: 初始化 + 宪法(略,参考前面案例)

Step 4: 规格

bash
/speckit.specify 待办事项 API,支持创建任务、标记完成、设置优先级、添加标签、过滤查询

spec.md(关键部分):

markdown
## User Scenarios

### P1: Create Task
**Acceptance Criteria**:
- Given 用户已认证
- When POST /api/tasks with {title, description, priority}
- Then 返回 201 with task ID
- And 任务保存到数据库
- And 响应时间 < 100ms

### P2: Mark Task Complete
**Acceptance Criteria**:
- Given 任务存在且未完成
- When PATCH /api/tasks/{id} with {completed: true}
- Then 返回 200 with updated task
- And completed_at timestamp 更新

### P3: Filter Tasks by Tag
**Acceptance Criteria**:
- Given 多个任务有不同标签
- When GET /api/tasks?tags=work,urgent
- Then 返回所有包含"work"或"urgent"标签的任务
- And 按优先级降序排序

## Requirements
- FR-001: API MUST 支持 JWT 认证
- FR-002: 所有端点 MUST 返回标准化错误格式
- FR-003: 任务 MUST 有优先级(1-5,1最高)
- FR-004: 标签 MUST 支持多个(逗号分隔)

## Success Criteria
- SC-001: API 响应时间 < 200ms (P95)
- SC-002: 支持 1000 并发请求
- SC-003: 测试覆盖率 > 90%

Step 5: 技术方案(契约优先)

bash
/speckit.plan FastAPI + PostgreSQL + JWT + pytest,先定义 OpenAPI 契约再实现

plan.md(重点:API 契约):

markdown
## Technical Context
- Language: Python 3.11+
- Framework: FastAPI 0.104+
- Database: PostgreSQL 15 + SQLAlchemy 2.0
- Authentication: JWT (PyJWT)
- Testing: pytest + httpx
- API Docs: Auto-generated from OpenAPI 3.0

## Phase 1: Design(设计优先)

### API Contract(OpenAPI 3.0)

**文件:** `specs/001-todo-api/contracts/api.yaml`

```yaml
openapi: 3.0.0
info:
  title: Todo API
  version: 1.0.0
  description: RESTful API for managing todo tasks

servers:
  - url: http://localhost:8000
    description: Development server

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Task:
      type: object
      required:
        - id
        - title
        - priority
        - completed
        - created_at
      properties:
        id:
          type: integer
          example: 42
        title:
          type: string
          minLength: 1
          maxLength: 200
          example: "Finish API documentation"
        description:
          type: string
          maxLength: 1000
          example: "Write comprehensive OpenAPI specs"
        priority:
          type: integer
          minimum: 1
          maximum: 5
          example: 1
        tags:
          type: array
          items:
            type: string
          example: ["work", "urgent"]
        completed:
          type: boolean
          example: false
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          example: "2025-01-15T10:30:00Z"
        updated_at:
          type: string
          format: date-time
          example: "2025-01-15T10:30:00Z"

    CreateTaskRequest:
      type: object
      required:
        - title
        - priority
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 1000
        priority:
          type: integer
          minimum: 1
          maximum: 5
          default: 3
        tags:
          type: array
          items:
            type: string

    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        priority:
          type: integer
          minimum: 1
          maximum: 5
        tags:
          type: array
          items:
            type: string
        completed:
          type: boolean

    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          example: "VALIDATION_ERROR"
        message:
          type: string
          example: "Invalid priority value"
        details:
          type: object

paths:
  /api/tasks:
    get:
      summary: List tasks
      operationId: listTasks
      tags: [Tasks]
      security:
        - BearerAuth: []
      parameters:
        - name: tags
          in: query
          schema:
            type: string
          description: Comma-separated tags (OR filter)
          example: "work,urgent"
        - name: completed
          in: query
          schema:
            type: boolean
          description: Filter by completion status
        - name: priority
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 5
          description: Filter by priority
        - name: sort
          in: query
          schema:
            type: string
            enum: [priority_desc, created_asc, created_desc]
            default: priority_desc
      responses:
        '200':
          description: List of tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  total:
                    type: integer
                    example: 42
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    post:
      summary: Create task
      operationId: createTask
      tags: [Tasks]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
      responses:
        '201':
          description: Task created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized

  /api/tasks/{task_id}:
    get:
      summary: Get task by ID
      operationId: getTask
      tags: [Tasks]
      security:
        - BearerAuth: []
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Task details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '404':
          description: Task not found

    patch:
      summary: Update task
      operationId: updateTask
      tags: [Tasks]
      security:
        - BearerAuth: []
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskRequest'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '404':
          description: Task not found

    delete:
      summary: Delete task
      operationId: deleteTask
      tags: [Tasks]
      security:
        - BearerAuth: []
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Task deleted
        '404':
          description: Task not found

关键点:契约优先

1. 先定义 OpenAPI 契约
2. 前后端基于契约并行开发
3. 用契约生成:
   - API 文档(自动)
   - 类型定义(Pydantic models)
   - Mock 服务器(前端测试用)
   - 集成测试(验证实现符合契约)

Data Model(基于契约生成)

python
# src/models/task.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ARRAY
from sqlalchemy.sql import func
from database import Base

class Task(Base):
    __tablename__ = 'tasks'

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, nullable=False, index=True)  # From JWT
    title = Column(String(200), nullable=False)
    description = Column(String(1000), nullable=True)
    priority = Column(Integer, nullable=False, default=3)  # 1-5
    tags = Column(ARRAY(String), nullable=True, default=[])
    completed = Column(Boolean, nullable=False, default=False)
    completed_at = Column(DateTime, nullable=True)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, onupdate=func.now())

    # Pydantic schema(用于 FastAPI validation)
    class Config:
        from_attributes = True

#### Step 7: 任务分解(契约驱动)

```markdown
## Phase 1: Setup
- [ ] T001 Initialize FastAPI project
- [ ] T002 [P] Create database schema migration
- [ ] T003 [P] Set up pytest with fixtures

## Phase 2: Contract Implementation
- [ ] T004 Generate Pydantic models from OpenAPI at /abs/path/src/schemas.py
  Tool: datamodel-code-generator --input api.yaml --output schemas.py

- [ ] T005 Create Task SQLAlchemy model at /abs/path/src/models/task.py
  Match OpenAPI schema fields

## Phase 3: Endpoints (Contract-Driven)

### Endpoint: POST /api/tasks
- [ ] T006 Write contract test at /abs/path/tests/test_create_task.py
  ```python
  def test_create_task_matches_contract():
      response = client.post("/api/tasks", json={...})
      assert response.status_code == 201
      # Validate response against OpenAPI schema
      validate_response(response.json(), "CreateTaskResponse")
  • [ ] T007 Implement endpoint at /abs/path/src/api/tasks.py
    python
    @app.post("/api/tasks", response_model=TaskResponse, status_code=201)
    async def create_task(task: CreateTaskRequest, user=Depends(get_current_user)):
        # Implementation

CHECKPOINT: Contract test passes

Endpoint: GET /api/tasks

  • [ ] T008 Write contract test for list endpoint
  • [ ] T009 Implement list with filtering

Endpoint: PATCH /api/tasks/

  • [ ] T010 Write contract test for update
  • [ ] T011 Implement update logic

Phase 4: Contract Validation

  • [ ] T012 Run OpenAPI validator against implementation Tool: openapi-spec-validator api.yaml

  • [ ] T013 Generate API documentation Tool: FastAPI auto-generated docs at /docs


#### Step 8: 实现(代码示例)

```python
# src/api/tasks.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from src.schemas import TaskResponse, CreateTaskRequest, UpdateTaskRequest
from src.models.task import Task
from src.dependencies import get_db, get_current_user

router = APIRouter(prefix="/api", tags=["Tasks"])

@router.post("/tasks", response_model=TaskResponse, status_code=201)
async def create_task(
    task_data: CreateTaskRequest,
    db: Session = Depends(get_db),
    current_user: int = Depends(get_current_user)
):
    """
    Create a new task.

    Implements: POST /api/tasks from OpenAPI contract
    """
    task = Task(
        user_id=current_user,
        title=task_data.title,
        description=task_data.description,
        priority=task_data.priority or 3,
        tags=task_data.tags or []
    )
    db.add(task)
    db.commit()
    db.refresh(task)
    return task

@router.get("/tasks", response_model=dict)
async def list_tasks(
    tags: Optional[str] = Query(None, description="Comma-separated tags"),
    completed: Optional[bool] = None,
    priority: Optional[int] = Query(None, ge=1, le=5),
    sort: str = Query("priority_desc", regex="^(priority_desc|created_asc|created_desc)$"),
    db: Session = Depends(get_db),
    current_user: int = Depends(get_current_user)
):
    """
    List tasks with filtering.

    Implements: GET /api/tasks from OpenAPI contract
    """
    query = db.query(Task).filter(Task.user_id == current_user)

    # Filter by tags (OR logic)
    if tags:
        tag_list = [t.strip() for t in tags.split(",")]
        query = query.filter(Task.tags.overlap(tag_list))

    # Filter by completed status
    if completed is not None:
        query = query.filter(Task.completed == completed)

    # Filter by priority
    if priority:
        query = query.filter(Task.priority == priority)

    # Sorting
    if sort == "priority_desc":
        query = query.order_by(Task.priority.asc())  # 1 is highest
    elif sort == "created_asc":
        query = query.order_by(Task.created_at.asc())
    elif sort == "created_desc":
        query = query.order_by(Task.created_at.desc())

    tasks = query.all()
    return {"tasks": tasks, "total": len(tasks)}

@router.patch("/tasks/{task_id}", response_model=TaskResponse)
async def update_task(
    task_id: int,
    task_data: UpdateTaskRequest,
    db: Session = Depends(get_db),
    current_user: int = Depends(get_current_user)
):
    """
    Update task.

    Implements: PATCH /api/tasks/{task_id} from OpenAPI contract
    """
    task = db.query(Task).filter(
        Task.id == task_id,
        Task.user_id == current_user
    ).first()

    if not task:
        raise HTTPException(status_code=404, detail="Task not found")

    # Update fields
    for field, value in task_data.dict(exclude_unset=True).items():
        if field == "completed" and value:
            task.completed_at = func.now()
        setattr(task, field, value)

    db.commit()
    db.refresh(task)
    return task

Step 9: 契约验证

python
# tests/test_contract_compliance.py
import pytest
from fastapi.testclient import TestClient
from openapi_spec_validator import validate_spec
from openapi_spec_validator.readers import read_from_filename

def test_openapi_contract_valid():
    """验证 OpenAPI 契约本身是合法的"""
    spec_dict, spec_url = read_from_filename('specs/001-todo-api/contracts/api.yaml')
    validate_spec(spec_dict)

def test_create_task_matches_contract(client: TestClient, auth_headers):
    """验证实现符合契约"""
    response = client.post(
        "/api/tasks",
        json={
            "title": "Test task",
            "description": "Test description",
            "priority": 1,
            "tags": ["work"]
        },
        headers=auth_headers
    )

    assert response.status_code == 201

    data = response.json()
    # 验证响应结构符合 OpenAPI schema
    assert "id" in data
    assert "title" in data
    assert data["title"] == "Test task"
    assert data["priority"] == 1
    assert "created_at" in data

def test_list_tasks_filtering(client: TestClient, auth_headers):
    """验证过滤功能符合契约"""
    # Create test tasks
    client.post("/api/tasks", json={"title": "Work task", "tags": ["work"], "priority": 1}, headers=auth_headers)
    client.post("/api/tasks", json={"title": "Personal task", "tags": ["personal"], "priority": 3}, headers=auth_headers)

    # Filter by tag
    response = client.get("/api/tasks?tags=work", headers=auth_headers)
    assert response.status_code == 200
    data = response.json()
    assert data["total"] == 1
    assert data["tasks"][0]["tags"] == ["work"]

    # Filter by priority
    response = client.get("/api/tasks?priority=1", headers=auth_headers)
    assert response.status_code == 200
    assert response.json()["total"] == 1

Step 10: API 文档自动生成

bash
# 启动服务器
uvicorn main:app --reload

# 访问自动生成的 API 文档
# http://localhost:8000/docs(Swagger UI)
# http://localhost:8000/redoc(ReDoc)

# 文档自动基于 OpenAPI 契约生成,与实现保持同步!

🎓 三个案例的核心差异

维度案例 1:个人作品集案例 2:相册管理器案例 3:待办 API
复杂度简单中等
耗时10 分钟30 分钟60 分钟
重点基础流程澄清 + 验证契约优先
宪法静态网站Offline-First + TypeScriptAPI 性能 + 安全
澄清1 个(表单处理)2 个(删除确认、数量限制)0 个(需求明确)
契约OpenAPI 3.0
测试Lighthouse单元测试 + E2E契约测试
技术栈HTML/CSS/JSReact + TS + IndexedDBFastAPI + PostgreSQL

💡 最佳实践总结

1. 从简单开始

第一次用 Spec-kit?

选择案例 1(个人作品集)

10 分钟体验完整流程

建立信心后尝试案例 2、3

2. 宪法的价值

有宪法:
  ✅ 团队统一标准
  ✅ AI 自动验证合规性
  ✅ 防止技术债务

无宪法:
  ❌ 每个开发者风格不同
  ❌ 代码质量参差不齐
  ❌ 后期重构成本高

3. 澄清的时机

何时澄清?
  ✅ spec.md 中有 [NEEDS CLARIFICATION]
  ✅ 需求模糊不清
  ✅ 多个技术方案需要选择

何时跳过?
  ❌ 需求已经非常明确
  ❌ 团队已有成熟方案

4. 契约优先的价值

案例 3 的关键:先定义 OpenAPI 契约

价值:
  ✅ 前后端并行开发(无需等待)
  ✅ 自动生成 API 文档
  ✅ 契约测试保证一致性
  ✅ 类型定义自动生成

传统方式:
  ❌ 后端实现完才能定义 API
  ❌ 前端等待后端
  ❌ 文档手工编写易过时

🚀 下一步

恭喜你完成了 3 个实战案例!现在你已经掌握:

✅ Spec-kit 基础工作流 ✅ 宪法(Constitution)的作用 ✅ 澄清(Clarification)流程 ✅ 契约优先(Contract-First)开发

下一步: 4.4 进阶技巧和最佳实践 - 学习团队协作、CI/CD 集成、多方案探索等高级技巧!


Spec-kit 核心概念教程 v1.0 | 2025 Edition

基于 MIT 许可证发布。内容版权归作者所有。