- name
- api-design
- description
- RESTful, GraphQL, gRPC, and API best practices
- domain
- software-design
- version
- 1.0.0
- tags
- ["rest","graphql","grpc","websocket","openapi","versioning"]
- triggers
- {"keywords":{"primary":["api","rest","restful","graphql","grpc","endpoint","openapi"],"secondary":["swagger","websocket","webhook","versioning","rate limit","pagination"]},"context_boost":["backend","microservice","integration","http"],"context_penalty":["frontend","ui","css"],"priority":"high"}
- collaboration
- {"prerequisites":[],"delegation_triggers":[{"trigger":"Implementation of API endpoints","delegate_to":"backend","context":"Framework-specific implementation patterns"},{"trigger":"Data model for API responses","delegate_to":"database","context":"Schema design, query requirements"},{"trigger":"API contract testing","delegate_to":"testing-strategies","context":"Contract tests, integration tests"}],"receives_context_from":[{"skill":"backend","receives":"[Truncated]"},{"skill":"database","receives":"[Truncated]"}],"provides_context_to":[{"skill":"backend","provides":"[Truncated]"},{"skill":"frontend","provides":"[Truncated]"},{"skill":"testing-strategies","provides":"[Truncated]"}]}
# 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 |
```typescript
// 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
```typescript
// 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
```typescript
// 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
```graphql
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
```typescript
// 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
```typescript
// 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
```protobuf
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
```typescript
// 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
```typescript
// 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)
```typescript
// 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)
```yaml
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 |
```typescript
// 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、沒有考慮常見使用場景
Ver en GitHub