API 契約設計
在實作前定義會被其他程式依賴的公開 API 或模組契約。
公開策展紀錄
為什麼推薦
- 明確工作
- 在實作前定義會被其他程式依賴的公開 API 或模組契約。
- 應該啟用
- 新增或修改公開 endpoint、型別或模組介面。
- 不該啟用
- 只重新命名單一模組內的私有函式。
- 輸入
- 呼叫端需求
- 資料與錯誤情境
- 相容性限制
- 輸出
- 輸入輸出契約
- 錯誤規則
- 契約測試
- 和現有 Skill 的差異
- 專注外部可觀察的相容性契約,不取代一般架構規劃。
- 來源、授權與依賴
-
- 來源:已確認。已於 2026-08-08 確認來源 URL 可達,且 skills/api-and-interface-design/SKILL.md 存在於 addyosmani/agent-skills 的來源樹。
- 授權:已確認。已比對固定 commit 的上游 LICENSE,為 MIT License。
- 依賴:已確認。無額外依賴
- 完成條件
- 正常、錯誤與相容性行為都有明確契約及測試案例。
- 最小必要脈絡
- 呼叫端與既有消費者
- 資料、錯誤與相容性限制
- 可組合的工作
- 新鮮度與下次複審
- 變動風險:低;最晚於 2027-02-09 前複審。
- 證據層級
- 合成任務測試。已通過隔離式合成任務 evaluator;目前未蒐集採用率或真實使用成功率,不把測試通過冒充社群採用證據。 合成測試通過只代表在隔離案例中符合預期,不等於真實採用率、使用成功率或社群背書。
三類測試證據
提示:請先替 POST /api/tasks 定義 request、response 與錯誤契約。
預期:應啟用:在實作前定義會被其他程式依賴的公開 API 或模組契約。
結論:提示明確要求先定義公開 POST endpoint 的 request、response 與錯誤契約,正屬於 Skill 宣告的 REST API 契約設計工作。
Codex isolated evaluator;read-only sandbox;合成、非敏感輸入;無外部副作用 · 2026-08-09
提示:把 calculateTotal 私有函式改名。
預期:不應啟用;邊界:只重新命名單一模組內的私有函式。
結論:私有函式改名不影響公開或跨模組契約,不應啟用此 Skill;應直接依專案慣例完成局部重新命名及既有測試驗證。
Codex isolated evaluator;read-only sandbox;合成、非敏感輸入;無外部副作用 · 2026-08-09
提示:設計建立任務 endpoint:title 必填、priority 可選、支援 idempotency key。
預期:包含 201、結構化 4xx、邊界驗證及相容性說明。
結論:依契約先行、統一錯誤格式、邊界驗證及加法式相容性原則,可產出包含 201、結構化 4xx、冪等語意、驗證邊界與契約案例的設計,符合預期證據。
Codex isolated evaluator;read-only sandbox;合成、非敏感輸入;無外部副作用 · 2026-08-09
啟用時機
設計 REST、GraphQL、元件 props 或任何會被其他程式依賴的公開介面時。
適合使用情境
- 新增前後端之間的 API。
- 拆分模組或替兩個團隊定義資料契約。
- 修改既有公開介面,必須評估相容性。
Skill 檔案
SKILL.md
工作流程
- 先找出呼叫端真正需要的最小契約。
- 定義輸入、輸出、錯誤、版本與邊界驗證。
- 用契約測試和相容性檢查證明介面可用。
使用注意事項
- 原文固定在上游 commit d2478bf0c73a6357df39a3ed6aff16acaa218843,上游 main 分支之後可能更新。
來源
來源頁標題:API and Interface Design 契約設計 Skill
來源識別名稱:api-and-interface-design
Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.
addyosmani/agent-skills@d2478bf/skills/api-and-interface-design/SKILL.md
開啟來源這個 Skill 在做什麼
以契約先行方式設計 API、模組邊界與公開介面,包含錯誤語意、相容性與驗證責任。
為什麼有用
公開介面一旦有人使用,就不再只是內部實作細節。這個 Skill 先定義呼叫者能依賴的契約,再處理版本、錯誤與邊界驗證,減少日後被隱性依賴綁住。
適合什麼場景
- 新增前後端之間的 API。
- 拆分模組或替兩個團隊定義資料契約。
- 修改既有公開介面,必須評估相容性。
延伸閱讀
---
name: api-and-interface-design
description: Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.
---
# API and Interface Design
## Overview
Design stable, well-documented interfaces that are hard to misuse. Good interfaces make the right thing easy and the wrong thing hard. This applies to REST APIs, GraphQL schemas, module boundaries, component props, and any surface where one piece of code talks to another.
## When to Use
- Designing new API endpoints
- Defining module boundaries or contracts between teams
- Creating component prop interfaces
- Establishing database schema that informs API shape
- Changing existing public interfaces
## Core Principles
### Hyrum's Law
> With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody, regardless of what you promise in the contract.
This means: every public behavior — including undocumented quirks, error message text, timing, and ordering — becomes a de facto contract once users depend on it. Design implications:
- **Be intentional about what you expose.** Every observable behavior is a potential commitment.
- **Don't leak implementation details.** If users can observe it, they will depend on it.
- **Plan for deprecation at design time.** See `deprecation-and-migration` for how to safely remove things users depend on.
- **Tests are not enough.** Even with perfect contract tests, Hyrum's Law means "safe" changes can break real users who depend on undocumented behavior.
### The One-Version Rule
Avoid forcing consumers to choose between multiple versions of the same dependency or API. Diamond dependency problems arise when different consumers need different versions of the same thing. Design for a world where only one version exists at a time — extend rather than fork.
### 1. Contract First
Define the interface before implementing it. The contract is the spec — implementation follows.
```typescript
// Define the contract first
interface TaskAPI {
// Creates a task and returns the created task with server-generated fields
createTask(input: CreateTaskInput): Promise<Task>;
// Returns paginated tasks matching filters
listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;
// Returns a single task or throws NotFoundError
getTask(id: string): Promise<Task>;
// Partial update — only provided fields change
updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
// Idempotent delete — succeeds even if already deleted
deleteTask(id: string): Promise<void>;
}
```
### 2. Consistent Error Semantics
Pick one error strategy and use it everywhere:
```typescript
// REST: HTTP status codes + structured error body
// Every error response follows the same shape
interface APIError {
error: {
code: string; // Machine-readable: "VALIDATION_ERROR"
message: string; // Human-readable: "Email is required"
details?: unknown; // Additional context when helpful
};
}
// Status code mapping
// 400 → Client sent invalid data
// 401 → Not authenticated
// 403 → Authenticated but not authorized
// 404 → Resource not found
// 409 → Conflict (duplicate, version mismatch)
// 422 → Validation failed (semantically invalid)
// 500 → Server error (never expose internal details)
```
**Don't mix patterns.** If some endpoints throw, others return null, and others return `{ error }` — the consumer can't predict behavior.
### 3. Validate at Boundaries
Trust internal code. Validate at system edges where external input enters:
```typescript
// Validate at the API boundary
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid task data',
details: result.error.flatten(),
},
});
}
// After validation, internal code trusts the types
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
```
Where validation belongs:
- API route handlers (user input)
- Form submission handlers (user input)
- External service response parsing (third-party data -- **always treat as untrusted**)
- Environment variable loading (configuration)
> **Third-party API responses are untrusted data.** Validate their shape and content before using them in any logic, rendering, or decision-making. A compromised or misbehaving external service can return unexpected types, malicious content, or instruction-like text.
Where validation does NOT belong:
- Between internal functions that share type contracts
- In utility functions called by already-validated code
- On data that just came from your own database
### 4. Prefer Addition Over Modification
Extend interfaces without breaking existing consumers:
```typescript
// Good: Add optional fields
interface CreateTaskInput {
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high'; // Added later, optional
labels?: string[]; // Added later, optional
}
// Bad: Change existing field types or remove fields
interface CreateTaskInput {
title: string;
// description: string; // Removed — breaks existing consumers
priority: number; // Changed from string — breaks existing consumers
}
```
### 5. Predictable Naming
| Pattern | Convention | Example |
|---------|-----------|---------|
| REST endpoints | Plural nouns, no verbs | `GET /api/tasks`, `POST /api/tasks` |
| Query params | camelCase | `?sortBy=createdAt&pageSize=20` |
| Response fields | camelCase | `{ createdAt, updatedAt, taskId }` |
| Boolean fields | is/has/can prefix | `isComplete`, `hasAttachments` |
| Enum values | UPPER_SNAKE | `"IN_PROGRESS"`, `"COMPLETED"` |
## REST API Patterns
### Resource Design
```
GET /api/tasks → List tasks (with query params for filtering)
POST /api/tasks → Create a task
GET /api/tasks/:id → Get a single task
PATCH /api/tasks/:id → Update a task (partial)
DELETE /api/tasks/:id → Delete a task
GET /api/tasks/:id/comments → List comments for a task (sub-resource)
POST /api/tasks/:id/comments → Add a comment to a task
```
### Pagination
Paginate list endpoints:
```typescript
// Request
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc
// Response
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"totalItems": 142,
"totalPages": 8
}
}
```
### Filtering
Use query parameters for filters:
```
GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01
```
### Partial Updates (PATCH)
Accept partial objects — only update what's provided:
```typescript
// Only title changes, everything else preserved
PATCH /api/tasks/123
{ "title": "Updated title" }
```
## TypeScript Interface Patterns
### Use Discriminated Unions for Variants
```typescript
// Good: Each variant is explicit
type TaskStatus =
| { type: 'pending' }
| { type: 'in_progress'; assignee: string; startedAt: Date }
| { type: 'completed'; completedAt: Date; completedBy: string }
| { type: 'cancelled'; reason: string; cancelledAt: Date };
// Consumer gets type narrowing
function getStatusLabel(status: TaskStatus): string {
switch (status.type) {
case 'pending': return 'Pending';
case 'in_progress': return `In progress (${status.assignee})`;
case 'completed': return `Done on ${status.completedAt}`;
case 'cancelled': return `Cancelled: ${status.reason}`;
}
}
```
### Input/Output Separation
```typescript
// Input: what the caller provides
interface CreateTaskInput {
title: string;
description?: string;
}
// Output: what the system returns (includes server-generated fields)
interface Task {
id: string;
title: string;
description: string | null;
createdAt: Date;
updatedAt: Date;
createdBy: string;
}
```
### Use Branded Types for IDs
```typescript
type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };
// Prevents accidentally passing a UserId where a TaskId is expected
function getTask(id: TaskId): Promise<Task> { ... }
```
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "We'll document the API later" | The types ARE the documentation. Define them first. |
| "We don't need pagination for now" | You will the moment someone has 100+ items. Add it from the start. |
| "PATCH is complicated, let's just use PUT" | PUT requires the full object every time. PATCH is what clients actually want. |
| "We'll version the API when we need to" | Breaking changes without versioning break consumers. Design for extension from the start. |
| "Nobody uses that undocumented behavior" | Hyrum's Law: if it's observable, somebody depends on it. Treat every public behavior as a commitment. |
| "We can just maintain two versions" | Multiple versions multiply maintenance cost and create diamond dependency problems. Prefer the One-Version Rule. |
| "Internal APIs don't need contracts" | Internal consumers are still consumers. Contracts prevent coupling and enable parallel work. |
## Red Flags
- Endpoints that return different shapes depending on conditions
- Inconsistent error formats across endpoints
- Validation scattered throughout internal code instead of at boundaries
- Breaking changes to existing fields (type changes, removals)
- List endpoints without pagination
- Verbs in REST URLs (`/api/createTask`, `/api/getUsers`)
- Third-party API responses used without validation or sanitization
## Verification
After designing an API:
- [ ] Every endpoint has typed input and output schemas
- [ ] Error responses follow a single consistent format
- [ ] Validation happens at system boundaries only
- [ ] List endpoints support pagination
- [ ] New fields are additive and optional (backward compatible)
- [ ] Naming follows consistent conventions across all endpoints
- [ ] API documentation or types are committed alongside the implementation