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
# 方式 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 portfolioStep 2: 初始化项目
# 创建项目
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/ # 即将创建
└── .gitignoreStep 3: 创建宪法(可选但推荐)
在 Claude Code 中输入:
/speckit.constitutionAI 会问你一些问题:
Q: 这个项目有什么不可协商的原则吗?
A: 静态网站,不用服务器,只用 HTML/CSS/JavaScript,不用任何前端框架生成的宪法(.specify/memory/constitution.md):
# 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 / VercelStep 4: 编写规格说明
/speckit.specify 个人作品集网站,包含首页、项目展示、技能列表、联系方式AI 生成的 specs/001-portfolio/spec.md:
# 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)
/speckit.clarifyAI 提问:
Question 1 of 2: Contact Form Message Handling
由于项目必须是静态网站(无服务器),联系表单消息如何处理?
Options:
A) 使用第三方服务(Formspree, Netlify Forms)
B) 仅显示邮箱地址,无实际表单
C) 使用 mailto: 链接打开邮件客户端
Recommended: A(第三方服务)- 用户体验最佳,符合静态架构
Your choice: [A]AI 更新 spec.md:
- FR-004: 联系表单 MUST 使用 Formspree 发送消息(免费 tier)
- Added Edge Case: Formspree 配额用完 → 显示降级到 mailto: 链接Step 6: 技术方案
/speckit.plan 用 Vite 作为构建工具,纯 HTML/CSS/JavaScript,Formspree 处理表单生成 specs/001-portfolio/plan.md(节选):
# 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 Structureportfolio/ ├── 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
// 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(节选):
# 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 ChromeStep 8: 实现
/speckit.implementAI 会按照 tasks.md 顺序自动生成代码:
- 初始化项目(T001-T003)
- 创建基础布局(T004-T006)
- 实现首页(T007-T009)
- 实现项目展示(T010-T013)
- 实现技能列表(T014-T015)
- 实现联系表单(T016-T018)
- 优化和打磨(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.pngStep 9: 验证
# 本地运行
npm run dev
# 打开浏览器
# http://localhost:5173
# 运行 Lighthouse 审计
# Chrome DevTools → Lighthouse → Generate Report
# 验证结果:
# ✅ Performance: 95
# ✅ Accessibility: 98
# ✅ Best Practices: 100
# ✅ SEO: 92Step 10: 部署
# 构建生产版本
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)
specify init photo-album --ai claude
cd photo-albumStep 3: 宪法
/speckit.constitution# 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: 规格
/speckit.specify 相册管理器,用户可以创建相册、上传照片、查看缩略图、删除照片生成的 spec.md(关键部分):
## 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: 澄清
/speckit.clarifyQ1: 删除照片确认
选项:
A) 需要二次确认(防误删)
B) 直接删除(快速操作)
C) 软删除 + 30 天恢复期
推荐:A
你的选择:A
更新:
- FR-015: 删除照片必须弹出确认对话框
- Edge Case: 误删 → 显示 Undo 提示(5 秒)Q2: 相册数量限制
推荐:50 个相册(避免 IndexedDB 过大)
你的选择:50
更新:
- FR-016: 相册数量限制 50 个
- Edge Case: 达到上限 → 显示"请删除旧相册"提示Step 6: 技术方案
/speckit.plan React 18 + TypeScript + Vite,用 Dexie.js 封装 IndexedDB生成的 plan.md(关键部分):
## 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)
// 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.taskstasks.md(节选):
## 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 renderingStep 8: 实现
/speckit.implementAI 按照 tasks 顺序生成所有代码(约 1,200 行 TypeScript/TSX)
Step 9: 验证
# 运行开发服务器
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: 规格
/speckit.specify 待办事项 API,支持创建任务、标记完成、设置优先级、添加标签、过滤查询spec.md(关键部分):
## 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: 技术方案(契约优先)
/speckit.plan FastAPI + PostgreSQL + JWT + pytest,先定义 OpenAPI 契约再实现plan.md(重点:API 契约):
## 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(基于契约生成)
# 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.pypython
@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 taskStep 9: 契约验证
# 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"] == 1Step 10: API 文档自动生成
# 启动服务器
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 + TypeScript | API 性能 + 安全 |
| 澄清 | 1 个(表单处理) | 2 个(删除确认、数量限制) | 0 个(需求明确) |
| 契约 | 无 | 无 | OpenAPI 3.0 |
| 测试 | Lighthouse | 单元测试 + E2E | 契约测试 |
| 技术栈 | HTML/CSS/JS | React + TS + IndexedDB | FastAPI + PostgreSQL |
💡 最佳实践总结
1. 从简单开始
第一次用 Spec-kit?
↓
选择案例 1(个人作品集)
↓
10 分钟体验完整流程
↓
建立信心后尝试案例 2、32. 宪法的价值
有宪法:
✅ 团队统一标准
✅ 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