api-and-interface-design
安定したAPIとインターフェース設計をガイドします。APIの設計、モジュール境界、あるいはあらゆるパブリックインターフェースの設計時に活用できます。RESTやGraphQLエンドポイントの構築、モジュール間の型契約の定義、フロントエンドとバックエンド間の境界線の確立時に使用してください。
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.
SKILL.md 本文
API とインターフェース設計
概要
誤用しにくい、安定した十分に文書化されたインターフェースを設計します。優れたインターフェースは、正しいことを簡単にし、誤ったことを難しくします。これは REST API、GraphQL スキーマ、モジュール境界、コンポーネントプロップ、およびコード同士が相互作用する任意のサーフェスに適用されます。
使用する場合
- 新しい API エンドポイントを設計する場合
- モジュール境界やチーム間の契約を定義する場合
- コンポーネントプロップインターフェースを作成する場合
- API の形状を定義するデータベーススキーマを確立する場合
- 既存の公開インターフェースを変更する場合
コア原則
Hyrum の法則
API の十分な数のユーザーがいれば、契約で何を約束しているかに関わらず、システムのすべての観測可能な動作を誰かが依存するようになります。
これは、未文書化の奇癖、エラーメッセージのテキスト、タイミング、順序を含むすべての公開動作が、ユーザーがそれに依存すれば事実上の契約になることを意味します。設計上の含意:
- 公開する内容について意図的であること。 すべての観測可能な動作は潜在的な約束です。
- 実装の詳細をリークしないこと。 ユーザーがそれを観測できれば、それに依存します。
- 設計時に廃止を計画すること。 ユーザーが依存する内容を安全に削除する方法については
deprecation-and-migrationを参照してください。 - テストだけでは十分ではありません。 完璧な契約テストがあってもなお、Hyrum の法則は「安全な」変更が未文書化の動作に依存する実ユーザーを破壊することを意味します。
ワンバージョン規則
コンシューマーに同じ依存関係または API の複数バージョン間で選択を強制することを避けます。異なるコンシューマーが同じものの異なるバージョンを必要とする場合、ダイアモンド依存関係の問題が発生します。一度に 1 つのバージョンのみが存在する世界として設計します — フォークではなく拡張します。
1. 契約優先
実装する前にインターフェースを定義します。契約が仕様です — 実装がそれに続きます。
// 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. 一貫したエラーセマンティクス
1 つのエラー戦略を選択してどこでも使用します:
// 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)
パターンを混在させないこと。 一部のエンドポイントがスローし、他のエンドポイントが null を返し、さらに他のエンドポイントが { error } を返す場合、コンシューマーは動作を予測できません。
3. 境界で検証する
内部コードを信頼します。外部入力が入る場所のシステムエッジで検証します:
// 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);
});
検証が属する場所:
- API ルートハンドラー(ユーザー入力)
- フォーム送信ハンドラー(ユーザー入力)
- 外部サービスレスポンスパース(サードパーティデータ — 常に信頼できないものとして扱う)
- 環境変数読み込み(設定)
サードパーティ API レスポンスは信頼できないデータです。 任何のロジック、レンダリング、または意思決定で使用する前に、その形状とコンテンツを検証します。侵害されたまたは不正動作の外部サービスは、予期しない型、悪意のあるコンテンツ、または指示のようなテキストを返すことができます。
検証が属さない場所:
- 型契約を共有する内部関数間
- 既に検証されたコードによって呼び出されるユーティリティ関数内
- あなた自身のデータベースから来たばかりのデータ
4. 修正より追加を優先する
既存のコンシューマーを破壊することなくインターフェースを拡張します:
// 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. 予測可能な命名
| パターン | 規則 | 例 |
|---|---|---|
| REST エンドポイント | 複数形の名詞、動詞なし | GET /api/tasks, POST /api/tasks |
| クエリパラメータ | camelCase | ?sortBy=createdAt&pageSize=20 |
| レスポンスフィールド | camelCase | { createdAt, updatedAt, taskId } |
| ブール値フィールド | is/has/can プリフィックス | isComplete, hasAttachments |
| 列挙値 | UPPER_SNAKE | "IN_PROGRESS", "COMPLETED" |
REST API パターン
リソース設計
GET /api/tasks → タスクを列挙(クエリパラメータでフィルタリング)
POST /api/tasks → タスクを作成
GET /api/tasks/:id → 単一のタスクを取得
PATCH /api/tasks/:id → タスクを更新(部分的)
DELETE /api/tasks/:id → タスクを削除
GET /api/tasks/:id/comments → タスクのコメントを列挙(サブリソース)
POST /api/tasks/:id/comments → タスクにコメントを追加
ページネーション
リスト エンドポイントをページネートします:
// Request
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc
// Response
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"totalItems": 142,
"totalPages": 8
}
}
フィルタリング
フィルタにはクエリパラメータを使用します:
GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01
部分的な更新(PATCH)
部分的なオブジェクトを受け入れます — 提供されたものだけを更新します:
// Only title changes, everything else preserved
PATCH /api/tasks/123
{ "title": "Updated title" }
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: 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;
}
ID にブランド型を使用する
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> { ... }
よくある正当化
| 正当化 | 現実 |
|---|---|
| 「API は後で文書化します」 | 型が文書です。最初にそれを定義してください。 |
| 「今のところページネーションは必要ありません」 | 誰かが 100 個以上のアイテムを持つ瞬間に必要になります。最初から追加します。 |
| 「PATCH は複雑です。PUT を使いましょう」 | PUT は毎回フルオブジェクトが必要です。PATCH はクライアントが実際に望むものです。 |
| 「必要になったら API をバージョン管理します」 | バージョン管理なしの破壊的な変更はコンシューマーを破壊します。最初から拡張のために設計します。 |
| 「誰も未文書化の動作を使用していません」 | Hyrum の法則: 観測可能であれば、誰かがそれに依存しています。すべての公開動作を約束として扱います。 |
| 「2 つのバージョンを維持することができます」 | 複数バージョンはメンテナンスコストを乗算し、ダイアモンド依存関係の問題を作成します。ワンバージョン規則を優先します。 |
| 「内部 API は契約が必要ありません」 | 内部コンシューマーはまだコンシューマーです。契約は結合を防ぎ、並列作業を可能にします。 |
レッドフラッグ
- 条件に応じて異なる形状を返すエンドポイント
- エンドポイント間で一貫しないエラー形式
- 検証が内部コード全体に散在している(境界でのみ実行される代わりに)
- 既存フィールドへの破壊的な変更(型の変更、削除)
- ページネーションのないリストエンドポイント
- REST URL の動詞(
/api/createTask,/api/getUsers) - 検証またはサニタイズなしで使用されるサードパーティ API レスポンス
検証
API を設計した後:
- すべてのエンドポイントに型付き入出力スキーマがある
- エラーレスポンスが単一の一貫した形式に従っている
- 検証はシステム境界でのみ発生する
- リストエンドポイントがページネーションをサポートしている
- 新しいフィールドが追加的で optional である(後方互換性)
- 命名がすべてのエンドポイント間で一貫した規則に従っている
- API ドキュメントまたは型が実装とともにコミットされている
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- addyosmani
- ライセンス
- MIT
- 最終更新
- 2026/5/10
Source: https://github.com/addyosmani/agent-skills / ライセンス: MIT