api-design

Design principles for building APIs that are intuitive, consistent, and scalable. Covers REST, GraphQL, gRPC, and real-time protocols.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-design" with this command: npx skills add miles990/claude-software-skills/miles990-claude-software-skills-api-design

API Design

Overview

Design principles for building APIs that are intuitive, consistent, and scalable. Covers REST, GraphQL, gRPC, and real-time protocols.

RESTful API Design

Resource Naming

✅ Good (nouns, plural): GET /users # List users GET /users/123 # Get user POST /users # Create user PUT /users/123 # Update user DELETE /users/123 # Delete user

❌ Bad (verbs, actions): GET /getUsers POST /createUser POST /users/123/delete

Nested Resources

Hierarchical relationship

GET /users/123/orders # User's orders GET /users/123/orders/456 # Specific order

Alternative: Query parameter for filtering

GET /orders?userId=123 # Filter orders by user

Rule: Nest max 2 levels deep

❌ /users/123/orders/456/items/789/reviews ✅ /order-items/789/reviews

HTTP Methods & Status Codes

Method Purpose Success Error

GET Read 200 404

POST Create 201 400, 409

PUT Replace 200 400, 404

PATCH Partial update 200 400, 404

DELETE Remove 204 404

// Response structure interface ApiResponse<T> { data: T; meta?: { page: number; limit: number; total: number; }; }

interface ApiError { error: { code: string; // Machine-readable message: string; // Human-readable details?: object; // Validation errors, etc. }; }

Pagination

// Offset-based (simple, has issues with large datasets) GET /users?page=2&limit=20

// Cursor-based (stable, performant) GET /users?cursor=eyJpZCI6MTIzfQ&limit=20

// Response { "data": [...], "meta": { "nextCursor": "eyJpZCI6MTQzfQ", "hasMore": true } }

Filtering & Sorting

// Query parameters GET /products?category=electronics&minPrice=100&maxPrice=500 GET /products?sort=-createdAt,name // - prefix for descending

// Filter operators GET /users?age[gte]=18&age[lte]=65 GET /users?status[in]=active,pending GET /users?name[like]=john*

GraphQL Design

Schema Design

type User { id: ID! email: String! name: String! posts(first: Int, after: String): PostConnection! createdAt: DateTime! }

type Post { id: ID! title: String! content: String! author: User! comments: [Comment!]! }

Connections for pagination (Relay spec)

type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! }

type PostEdge { node: Post! cursor: String! }

type Query { user(id: ID!): User users(first: Int, after: String, filter: UserFilter): UserConnection! }

type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! }

input CreateUserInput { email: String! name: String! password: String! }

type CreateUserPayload { user: User errors: [Error!] }

Resolver Patterns

// N+1 prevention with DataLoader const userLoader = new DataLoader(async (ids: string[]) => { const users = await db.users.findMany({ where: { id: { in: ids } } }); return ids.map(id => users.find(u => u.id === id)); });

const resolvers = { Post: { author: (post, _, { loaders }) => loaders.user.load(post.authorId), }, };

// Field-level authorization const resolvers = { User: { email: (user, _, { currentUser }) => { if (currentUser.id !== user.id && !currentUser.isAdmin) { return null; // Hide from other users } return user.email; }, }, };

Error Handling

// Union types for expected errors type CreatePostResult = Post | ValidationError | NotAuthorizedError

// Or use errors field in payload type CreatePostPayload { post: Post errors: [CreatePostError!] }

union CreatePostError = ValidationError | RateLimitError

gRPC Design

Protocol Buffers

syntax = "proto3";

package users.v1;

service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);

// Streaming rpc WatchUsers(WatchUsersRequest) returns (stream UserEvent); rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateResponse); }

message User { string id = 1; string email = 2; string name = 3; google.protobuf.Timestamp created_at = 4; }

message GetUserRequest { string id = 1; }

message GetUserResponse { User user = 1; }

message ListUsersRequest { int32 page_size = 1; string page_token = 2; UserFilter filter = 3; }

message ListUsersResponse { repeated User users = 1; string next_page_token = 2; }

When to Use gRPC

Use Case REST GraphQL gRPC

Public API ✅ ✅ ❌

Mobile apps ✅ ✅ ⚠️

Microservices ⚠️ ❌ ✅

Real-time ❌ ⚠️ ✅

Browser clients ✅ ✅ ⚠️

API Versioning

Strategies

URL versioning (most common)

GET /v1/users GET /v2/users

Header versioning

GET /users Accept: application/vnd.api+json; version=2

Query parameter

GET /users?version=2

Breaking vs Non-Breaking Changes

Non-Breaking (safe): ✅ Add new optional field ✅ Add new endpoint ✅ Add new optional query parameter ✅ Expand enum values (if client ignores unknown)

Breaking (requires new version): ❌ Remove field ❌ Rename field ❌ Change field type ❌ Make optional field required ❌ Change URL structure

Deprecation Strategy

// OpenAPI deprecation /**

  • @deprecated Use /v2/users instead. Will be removed on 2025-06-01. */ app.get('/v1/users', ...);

// Response header res.setHeader('Deprecation', 'true'); res.setHeader('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT'); res.setHeader('Link', '</v2/users>; rel="successor-version"');

Real-time APIs

WebSocket

// Server wss.on('connection', (ws) => { ws.on('message', (data) => { const message = JSON.parse(data);

switch (message.type) {
  case 'subscribe':
    subscriptions.add(ws, message.channel);
    break;
  case 'unsubscribe':
    subscriptions.remove(ws, message.channel);
    break;
}

}); });

// Client const ws = new WebSocket('wss://api.example.com/ws');

ws.send(JSON.stringify({ type: 'subscribe', channel: 'orders:user:123' }));

ws.onmessage = (event) => { const data = JSON.parse(event.data); handleUpdate(data); };

Server-Sent Events (SSE)

// Server app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive');

const sendEvent = (data: object) => { res.write(data: ${JSON.stringify(data)}\n\n); };

// Subscribe to events eventEmitter.on('update', sendEvent);

req.on('close', () => { eventEmitter.off('update', sendEvent); }); });

// Client const source = new EventSource('/events'); source.onmessage = (event) => { const data = JSON.parse(event.data); handleUpdate(data); };

API Documentation

OpenAPI (Swagger)

openapi: 3.0.3 info: title: User API version: 1.0.0

paths: /users: get: summary: List users parameters: - name: limit in: query schema: type: integer default: 20 responses: '200': description: Success content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/User'

components: schemas: User: type: object required: [id, email, name] properties: id: type: string format: uuid email: type: string format: email name: type: string

Security Best Practices

Practice Implementation

Authentication Bearer tokens, API keys

Rate limiting X-RateLimit-* headers

Input validation Schema validation (Zod, Joi)

CORS Whitelist allowed origins

HTTPS Always in production

Request IDs X-Request-ID for tracing

// Rate limit headers res.setHeader('X-RateLimit-Limit', '100'); res.setHeader('X-RateLimit-Remaining', '95'); res.setHeader('X-RateLimit-Reset', '1640000000');

Related Skills

  • [[architecture-patterns]] - API Gateway, microservices

  • [[security-practices]] - Authentication, authorization

  • [[documentation]] - API documentation tools

Sharp Edges(常見陷阱)

這些是 API 設計中最常見且代價最高的錯誤

SE-1: 破壞性變更 (Breaking Changes)

  • 嚴重度: critical

  • 情境: 修改現有 API 導致客戶端崩潰,沒有適當的版本控制

  • 原因: 刪除欄位、改變欄位類型、修改必填性、重新命名

  • 症狀:

  • 客戶端突然出現錯誤

  • 舊版 App 無法使用

  • 用戶投訴「昨天還能用,今天就壞了」

  • 檢測: -.*field|rename.*property|required.true.→.*false|type.string.→.*number

  • 解法: 使用 API 版本控制、只做 additive changes、設定 deprecation 期限

SE-2: 過度取得 (Over-fetching)

  • 嚴重度: medium

  • 情境: API 回傳太多客戶端不需要的資料

  • 原因: 「反正都有就回傳」的心態、沒有考慮不同使用場景

  • 症狀:

  • API 響應很大但客戶端只用其中一小部分

  • 行動裝置載入緩慢

  • 頻寬浪費

  • 檢測: select\s**|findMany()|findAll()(?!.*select)

  • 解法: 使用 fields selection、GraphQL、按需求設計 endpoint

SE-3: 取得不足 (Under-fetching)

  • 嚴重度: medium

  • 情境: 需要呼叫多個 API 才能取得完整資料

  • 原因: 過度細分 endpoint、沒有考慮常見使用場景

  • 症狀:

  • 前端需要 5+ 個 API 呼叫才能渲染一個頁面

  • 複雜的前端資料整合邏輯

  • N+1 API 請求問題

  • 檢測: Promise.all(.*fetch.*fetch.*fetch|.then(.*fetch

  • 解法: 設計聚合 endpoint、使用 include/expand 參數、考慮 BFF pattern

SE-4: 不一致的錯誤格式

  • 嚴重度: high

  • 情境: 不同 endpoint 回傳不同格式的錯誤,客戶端難以統一處理

  • 原因: 沒有統一的錯誤處理規範、不同開發者各自實作

  • 症狀:

  • 有的錯誤用 error ,有的用 message ,有的用 errors

  • HTTP status code 使用不一致

  • 客戶端需要寫很多 if-else 處理不同錯誤格式

  • 檢測: res.json({.*error|res.json({.*message|res.status(500).*error

  • 解法: 定義統一的錯誤回應格式、使用 global error handler、建立錯誤碼系統

SE-5: 缺乏 Rate Limiting

  • 嚴重度: critical

  • 情境: API 沒有請求頻率限制,容易被濫用或攻擊

  • 原因: 「先做出來再說」、不了解風險

  • 症狀:

  • DDoS 攻擊導致服務癱瘓

  • 單一用戶耗盡所有資源

  • 雲端帳單爆炸

  • 檢測: app.use((?!.*rateLimit)|router.(?!.*limit)|express()(?!.*rate)

  • 解法: 實作 rate limiting middleware、使用 Redis 追蹤請求、設定合理的限制

Validations

V-1: 禁止 HTTP status 200 回傳錯誤

  • 類型: regex

  • 嚴重度: high

  • 模式: res.json(\s*{\serror|res.send(\s{\s*error|.json({.success:\sfalse

  • 訊息: Error responses should use appropriate HTTP status codes (4xx/5xx)

  • 修復建議: Use res.status(400).json({ error: ... }) for client errors

  • 適用: *.ts , *.js

V-2: API 路徑使用動詞

  • 類型: regex

  • 嚴重度: medium

  • 模式: (get|create|update|delete|fetch|remove|add)/|/get|/create|/update|/delete

  • 訊息: REST API paths should use nouns, not verbs (use HTTP methods instead)

  • 修復建議: Change /getUsers to GET /users , /createUser to POST /users

  • 適用: *.ts , *.js

V-3: 缺少輸入驗證

  • 類型: regex

  • 嚴重度: critical

  • 模式: req.body.\w+(?!.*validate|.*schema|.*zod|.*joi|.*yup)

  • 訊息: Request body accessed without apparent validation

  • 修復建議: Add validation with Zod/Joi before using request data

  • 適用: *.ts , *.js

V-4: 硬編碼 HTTP status code

  • 類型: regex

  • 嚴重度: low

  • 模式: res.status(\d{3})

  • 訊息: Consider using named status codes for readability

  • 修復建議: Use HttpStatus.OK or constants instead of magic numbers

  • 適用: *.ts , *.js

V-5: 缺少 API 版本控制

  • 類型: regex

  • 嚴重度: medium

  • 模式: app.(get|post|put|delete)\s*(\s*['"]/(?!v\d|api/v\d)

  • 訊息: API route missing version prefix

  • 修復建議: Add version prefix: /api/v1/users or use header-based versioning

  • 適用: *.ts , *.js

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

saas-platforms

No summary provided by upstream source.

Repository SourceNeeds Review
General

architecture-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend

No summary provided by upstream source.

Repository SourceNeeds Review
General

project-management

No summary provided by upstream source.

Repository SourceNeeds Review