api-design-patterns by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill api-design-patterns使用经过验证的模式设计稳健、可扩展的 API,适用于 REST、GraphQL 和 gRPC,并包含适当的版本控制、身份验证和错误处理。
API 风格选择 :
关键模式 :
/v1/users)、请求头(Accept: application/vnd.api+json;version=1)、内容协商深入探讨请参阅 references/ : rest-patterns.md, graphql-patterns.md, grpc-patterns.md, versioning-strategies.md,
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
authentication.md在所有 API 风格中应用这些原则:
1. 一致性优于巧妙性
2. 为演进而设计
3. 默认安全
4. 开发者体验优先
✅ 在以下情况使用 REST:
❌ 在以下情况避免使用 REST:
示例用例:公共 API、移动后端、传统 Web 服务
✅ 在以下情况使用 GraphQL:
❌ 在以下情况避免使用 GraphQL:
示例用例:面向客户的 API、仪表板、具有多样化 UI 的移动应用
✅ 在以下情况使用 gRPC:
❌ 在以下情况避免使用 gRPC:
示例用例:内部微服务、流式数据、服务网格
✅ 良好实践:复数名词,分层结构
GET /users # 列出用户
GET /users/123 # 获取用户
POST /users # 创建用户
PUT /users/123 # 更新用户(完整)
PATCH /users/123 # 更新用户(部分)
DELETE /users/123 # 删除用户
GET /users/123/orders # 用户的订单(子资源)
❌ 不良实践:动词,混合约定
GET /getUsers # 不要使用动词
POST /user/create # 不要使用动词
GET /Users/123 # 不要大写
GET /user/123 # 不要混用单数/复数
成功码 :
200 OK:成功的 GET、PUT、PATCH、DELETE(带响应体)201 Created:成功的 POST,返回 Location 请求头202 Accepted:异步操作已启动204 No Content:成功的 DELETE,无响应体客户端错误码 :
400 Bad Request:无效输入,验证错误401 Unauthorized:缺少或无效的身份验证403 Forbidden:已认证但权限不足404 Not Found:资源不存在409 Conflict:状态冲突(重复、版本不匹配)422 Unprocessable Entity:语义验证错误429 Too Many Requests:超出速率限制服务器错误码 :
500 Internal Server Error:意外错误502 Bad Gateway:上游服务错误503 Service Unavailable:临时中断504 Gateway Timeout:上游超时✅ 一致的错误结构
{
"error": {
"code": "VALIDATION_ERROR",
"message": "无效的请求参数",
"details": [
{
"field": "email",
"message": "无效的电子邮件格式",
"code": "INVALID_FORMAT"
}
],
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}
偏移分页(简单,熟悉):
GET /users?limit=20&offset=40
✅ 适用于:小型数据集,管理界面 ❌ 避免用于:大型数据集(跳过操作变得昂贵),实时数据
游标分页(稳定,高效):
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }
✅ 适用于:无限滚动,实时信息流,大型数据集 ❌ 避免用于:随机访问,页码
键集分页(高性能):
GET /users?limit=20&after_id=123
✅ 适用于:有序数据,对数据库索引友好 ❌ 避免用于:复杂排序,多个排序键
有关过滤、排序、字段选择、HATEOAS,请参阅 references/rest-patterns.md
✅ 良好实践:清晰类型,默认可为空
type User {
id: ID! # 非空 ID
email: String! # 必填字段
name: String # 可选(默认可为空)
createdAt: DateTime!
orders: [Order!]! # 非空订单的非空数组
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
input CreateUserInput {
email: String!
name: String
}
type CreateUserPayload {
user: User
userEdge: UserEdge
errors: [UserError!]
}
使用 DataLoader 避免 N+1 查询 :
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.users.findMany({ where: { id: { in: userIds } } });
return userIds.map(id => users.find(u => u.id === id));
});
// 解析器自动批量查询
const resolvers = {
Order: {
user: (order) => userLoader.load(order.userId)
}
};
防止昂贵的查询:
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
schema,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});
有关订阅、Relay 游标连接、错误处理,请参阅 references/graphql-patterns.md
syntax = "proto3";
package users.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User) {}
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
rpc CreateUser (CreateUserRequest) returns (User) {}
rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "需要用户 ID")
}
user, err := s.db.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "未找到用户")
}
return nil, status.Error(codes.Internal, "数据库错误")
}
return user, nil
}
有关流式传输、拦截器、元数据、健康检查,请参阅 references/grpc-patterns.md
✅ 最常见,易于理解
GET /v1/users/123
GET /v2/users/123
优点:清晰,易于路由,对浏览器友好 缺点:将版本与 URL 耦合,重复路由
GET /users/123
Accept: application/vnd.myapi.v2+json
优点:简洁的 URL,版本与资源分离 缺点:不太明显,手动测试更困难
GET /users/123
Accept: application/vnd.myapi.user.v2+json
优点:资源级版本控制,向后兼容 缺点:复杂,实现更困难
{
"version": "1.0",
"deprecated": true,
"sunset_date": "2025-12-31",
"migration_guide": "https://docs.api.com/v1-to-v2",
"replacement_version": "2.0"
}
包含弃用警告 :
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"
有关详细的迁移模式,请参阅 references/versioning-strategies.md
适用于:第三方访问,用户同意,令牌刷新
授权码流程(对 Web/移动端最安全):
1. 客户端重定向到 /authorize
2. 用户进行身份验证,授予权限
3. 授权服务器重定向到回调地址并附带 code
4. 客户端用 code 交换访问令牌
5. 客户端使用访问令牌进行 API 请求
# 请求令牌
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
# 响应
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
# 使用令牌
GET /v1/users/me
Authorization: Bearer eyJhbGc...
适用于:微服务,无状态 API 身份验证,短期令牌
✅ 良好实践:最小化声明,短期有效期
{
"sub": "user_123",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read:users write:orders"
}
验证 :
import jwt from 'jsonwebtoken';
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = payload.sub;
适用于:服务器到服务器,CLI 工具,Webhook
GET /v1/users
X-API-Key: sk_live_abc123...
# 或查询参数(安全性较低)
GET /v1/users?api_key=sk_live_abc123
关键实践 :
sk_live_、sk_test_)有关 API 密钥轮换、作用域、RBAC,请参阅 references/authentication.md
桶:100 个令牌,每秒补充 10 个
请求消耗 1 个令牌
允许突发,最高可达桶大小
请求头 :
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200
429 响应 :
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "超出速率限制。请在 60 秒后重试。",
"limit": 100,
"reset_at": "2025-01-01T00:00:00Z"
}
}
在滚动时间窗口内统计请求数。比固定窗口更精确。
天然幂等:GET、PUT、DELETE、HEAD、OPTIONS 非幂等:POST、PATCH
使 POST 请求具有幂等性:
POST /v1/payments
Idempotency-Key: uuid-or-client-generated-key
Content-Type: application/json
{
"amount": 1000,
"currency": "USD",
"customer": "cust_123"
}
服务器行为 :
实现 :
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return res.status(cached.status).json(cached.body);
}
}
const result = await processPayment(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
status: 201,
body: result
});
使用 ETag 进行安全更新:
# 获取资源及其 ETag
GET /v1/users/123
Response: ETag: "abc123"
# 仅在未更改时更新
PUT /v1/users/123
If-Match: "abc123"
# 如果 ETag 已更改,则返回 412 Precondition Failed
# 公共,可缓存 1 小时
Cache-Control: public, max-age=3600
# 私有(用户特定),重新验证
Cache-Control: private, must-revalidate, max-age=0
# 不缓存
Cache-Control: no-store, no-cache, must-revalidate
# 服务器返回 ETag
GET /v1/users/123
Response:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600
# 客户端条件请求
GET /v1/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 如果未更改,则返回 304 Not Modified(节省带宽)
HTTP/1.1 304 Not Modified
GET /v1/users/123
Response:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
# 条件请求
GET /v1/users/123
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# 如果未修改,则返回 304 Not Modified
POST https://client.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1640995200
{
"id": "evt_abc123",
"type": "payment.succeeded",
"created": 1640995200,
"data": {
"object": {
"id": "pay_123",
"amount": 1000,
"status": "succeeded"
}
}
}
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required: [id, email]
properties:
id:
type: string
email:
type: string
format: email
name:
type: string
GraphQL 内省提供自动文档。使用描述:
"""
表示系统中的用户账户。
通过 createUser 突变创建。
"""
type User {
"""用户的唯一标识符"""
id: ID!
"""电子邮件地址,必须唯一"""
email: String!
"""可选的显示名称"""
name: String
}
❌ 过度获取(REST):当字段未使用时返回整个对象 ✅ 解决方案:支持字段选择(?fields=id,name,email)
❌ 获取不足(REST):获取相关数据需要多次请求 ✅ 解决方案:支持扩展(?expand=orders,profile)或使用 GraphQL
❌ 琐碎 API:常见操作需要太多往返通信 ✅ 解决方案:批量端点、复合文档或 GraphQL
❌ 忽略 HTTP 语义:使用 GET 进行变更、错误的状态码 ✅ 解决方案:遵循 HTTP 规范,使用正确的方法和状态码
❌ 暴露内部结构:URL/模式镜像数据库 ✅ 解决方案:设计与存储无关的面向资源的 API
❌ 缺少版本控制:进行破坏性变更而不增加版本号 ✅ 解决方案:从一开始就进行版本控制,绝不破坏现有版本
❌ 错误信息不佳:通用的“发生错误” ✅ 解决方案:提供具体的、可操作的错误信息及代码
❌ 没有速率限制:API 易受滥用 ✅ 解决方案:从一开始就实施速率限制
// Pact 契约测试
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserAPI'
});
it('gets a user by ID', () => {
provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123'
})
.willRespondWith({
status: 200,
body: { id: '123', email: 'user@example.com' }
});
});
// k6 负载测试
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '10s', target: 0 }
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 低于 500ms
http_req_failed: ['rate<0.01'] // <1% 错误率
}
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500
});
}
每周安装数
208
代码仓库
GitHub 星标数
27
首次出现
Jan 23, 2026
安全审计
安装于
gemini-cli175
opencode175
cursor172
codex169
github-copilot164
claude-code161
Design robust, scalable APIs using proven patterns for REST, GraphQL, and gRPC with proper versioning, authentication, and error handling.
API Style Selection :
Critical Patterns :
/v1/users), header (Accept: application/vnd.api+json;version=1), content negotiationSee references/ for deep dives : rest-patterns.md, graphql-patterns.md, grpc-patterns.md, versioning-strategies.md, authentication.md
Apply these principles across all API styles:
1. Consistency Over Cleverness
2. Design for Evolution
3. Security by Default
4. Developer Experience First
✅ Use REST when:
❌ Avoid REST when:
Example Use Cases : Public APIs, mobile backends, traditional web services
✅ Use GraphQL when:
❌ Avoid GraphQL when:
Example Use Cases : Client-facing APIs, dashboards, mobile apps with varied UIs
✅ Use gRPC when:
❌ Avoid gRPC when:
Example Use Cases : Internal microservices, streaming data, service mesh
✅ Good: Plural nouns, hierarchical
GET /users # List users
GET /users/123 # Get user
POST /users # Create user
PUT /users/123 # Update user (full)
PATCH /users/123 # Update user (partial)
DELETE /users/123 # Delete user
GET /users/123/orders # User's orders (sub-resource)
❌ Bad: Verbs, mixed conventions
GET /getUsers # Don't use verbs
POST /user/create # Don't use verbs
GET /Users/123 # Don't capitalize
GET /user/123 # Don't mix singular/plural
Success Codes :
200 OK: Successful GET, PUT, PATCH, DELETE with body201 Created: Successful POST, return Location header202 Accepted: Async operation started204 No Content: Successful DELETE, no bodyClient Error Codes :
400 Bad Request: Invalid input, validation error401 Unauthorized: Missing or invalid authentication403 Forbidden: Authenticated but insufficient permissions404 Not Found: Resource doesn't exist409 Conflict: State conflict (duplicate, version mismatch)422 Unprocessable Entity: Semantic validation error429 Too Many Requests: Rate limit exceededServer Error Codes :
500 Internal Server Error: Unexpected error502 Bad Gateway: Upstream service error503 Service Unavailable: Temporary outage504 Gateway Timeout: Upstream timeout✅ Consistent error structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}
Offset Pagination (simple, familiar):
GET /users?limit=20&offset=40
✅ Use for: Small datasets, admin interfaces ❌ Avoid for: Large datasets (skips become expensive), real-time data
Cursor Pagination (stable, efficient):
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response: { "data": [...], "next_cursor": "eyJpZCI6MTQzfQ" }
✅ Use for: Infinite scroll, real-time feeds, large datasets ❌ Avoid for: Random access, page numbers
Keyset Pagination (performant):
GET /users?limit=20&after_id=123
✅ Use for: Ordered data, database index friendly ❌ Avoid for: Complex sorting, multiple sort keys
See references/rest-patterns.md for filtering, sorting, field selection, HATEOAS
✅ Good: Clear types, nullable by default
type User {
id: ID! # Non-null ID
email: String! # Required field
name: String # Optional (nullable by default)
createdAt: DateTime!
orders: [Order!]! # Non-null array of non-null orders
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
input CreateUserInput {
email: String!
name: String
}
type CreateUserPayload {
user: User
userEdge: UserEdge
errors: [UserError!]
}
Avoid N+1 Queries with DataLoader :
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.users.findMany({ where: { id: { in: userIds } } });
return userIds.map(id => users.find(u => u.id === id));
});
// Resolver batches queries automatically
const resolvers = {
Order: {
user: (order) => userLoader.load(order.userId)
}
};
Prevent expensive queries:
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
schema,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});
See references/graphql-patterns.md for subscriptions, relay cursor connections, error handling
syntax = "proto3";
package users.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User) {}
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {}
rpc CreateUser (CreateUserRequest) returns (User) {}
rpc StreamUsers (StreamUsersRequest) returns (stream User) {}
rpc BidiChat (stream ChatMessage) returns (stream ChatMessage) {}
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "user ID is required")
}
user, err := s.db.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "database error")
}
return user, nil
}
See references/grpc-patterns.md for streaming, interceptors, metadata, health checks
✅ Most common, easy to understand
GET /v1/users/123
GET /v2/users/123
Pros : Clear, easy to route, browser-friendly Cons : Couples version to URL, duplicates routes
GET /users/123
Accept: application/vnd.myapi.v2+json
Pros : Clean URLs, version separate from resource Cons : Less visible, harder to test manually
GET /users/123
Accept: application/vnd.myapi.user.v2+json
Pros : Resource-level versioning, backward compatible Cons : Complex, harder to implement
{
"version": "1.0",
"deprecated": true,
"sunset_date": "2025-12-31",
"migration_guide": "https://docs.api.com/v1-to-v2",
"replacement_version": "2.0"
}
Include deprecation warnings :
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.com/v1-to-v2>; rel="deprecation"
See references/versioning-strategies.md for detailed migration patterns
Use for : Third-party access, user consent, token refresh
Authorization Code Flow (most secure for web/mobile):
1. Client redirects to /authorize
2. User authenticates, grants permissions
3. Auth server redirects to callback with code
4. Client exchanges code for access token
5. Client uses access token for API requests
# Request token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
# Response
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
# Use token
GET /v1/users/me
Authorization: Bearer eyJhbGc...
Use for : Microservices, stateless API auth, short-lived tokens
✅ Good: Minimal claims, short expiry
{
"sub": "user_123",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read:users write:orders"
}
Validation :
import jwt from 'jsonwebtoken';
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = payload.sub;
Use for : Server-to-server, CLI tools, webhooks
GET /v1/users
X-API-Key: sk_live_abc123...
# Or query parameter (less secure)
GET /v1/users?api_key=sk_live_abc123
Key Practices :
sk_live_, sk_test_)See references/authentication.md for API key rotation, scopes, RBAC
Bucket: 100 tokens, refill 10/second
Request costs 1 token
Allows bursts up to bucket size
Headers :
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640995200
429 Response :
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"limit": 100,
"reset_at": "2025-01-01T00:00:00Z"
}
}
Counts requests in rolling time window. More accurate than fixed window.
Naturally Idempotent : GET, PUT, DELETE, HEAD, OPTIONS Not Idempotent : POST, PATCH
Make POST requests idempotent:
POST /v1/payments
Idempotency-Key: uuid-or-client-generated-key
Content-Type: application/json
{
"amount": 1000,
"currency": "USD",
"customer": "cust_123"
}
Server behavior :
Implementation :
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return res.status(cached.status).json(cached.body);
}
}
const result = await processPayment(req.body);
await redis.setex(`idempotency:${idempotencyKey}`, 86400, {
status: 201,
body: result
});
Use ETags for safe updates:
# Get resource with ETag
GET /v1/users/123
Response: ETag: "abc123"
# Update only if unchanged
PUT /v1/users/123
If-Match: "abc123"
# 412 Precondition Failed if ETag changed
# Public, cacheable for 1 hour
Cache-Control: public, max-age=3600
# Private (user-specific), revalidate
Cache-Control: private, must-revalidate, max-age=0
# No caching
Cache-Control: no-store, no-cache, must-revalidate
# Server returns ETag
GET /v1/users/123
Response:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600
# Client conditional request
GET /v1/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 304 Not Modified if unchanged (saves bandwidth)
HTTP/1.1 304 Not Modified
GET /v1/users/123
Response:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
# Conditional request
GET /v1/users/123
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# 304 Not Modified if not modified
POST https://client.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1640995200
{
"id": "evt_abc123",
"type": "payment.succeeded",
"created": 1640995200,
"data": {
"object": {
"id": "pay_123",
"amount": 1000,
"status": "succeeded"
}
}
}
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
type: object
required: [id, email]
properties:
id:
type: string
email:
type: string
format: email
name:
type: string
GraphQL introspection provides automatic documentation. Use descriptions:
"""
Represents a user account in the system.
Created via the createUser mutation.
"""
type User {
"""Unique identifier for the user"""
id: ID!
"""Email address, must be unique"""
email: String!
"""Optional display name"""
name: String
}
❌ Over-fetching (REST) : Returning entire objects when fields are unused ✅ Solution : Support field selection (?fields=id,name,email)
❌ Under-fetching (REST) : Requiring multiple requests for related data ✅ Solution : Support expansion (?expand=orders,profile) or use GraphQL
❌ Chatty APIs : Too many round-trips for common operations ✅ Solution : Batch endpoints, compound documents, or GraphQL
❌ Ignoring HTTP semantics : Using GET for mutations, wrong status codes ✅ Solution : Follow HTTP spec, use correct methods and status codes
❌ Exposing internal structure : URLs/schemas mirror database ✅ Solution : Design resource-oriented APIs independent of storage
❌ Missing versioning : Breaking changes without version increments ✅ Solution : Version from day one, never break existing versions
❌ Poor error messages : Generic "An error occurred" ✅ Solution : Specific, actionable error messages with codes
❌ No rate limiting : APIs vulnerable to abuse ✅ Solution : Implement rate limiting from the start
// Pact contract test
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'FrontendApp',
provider: 'UserAPI'
});
it('gets a user by ID', () => {
provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123'
})
.willRespondWith({
status: 200,
body: { id: '123', email: 'user@example.com' }
});
});
// k6 load test
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '10s', target: 0 }
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'] // <1% errors
}
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500
});
}
Weekly Installs
208
Repository
GitHub Stars
27
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli175
opencode175
cursor172
codex169
github-copilot164
claude-code161
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
150,000 周安装
CI/CD流水线专家指南:GitHub Actions、GitLab CI、Jenkins安全高效部署
446 周安装
Diátaxis技术文档框架指南:创建教程、操作指南、参考与解释说明
455 周安装
Aceternity UI - Next.js 13+ 动画React组件库 | Tailwind CSS & Framer Motion
455 周安装
Anthropic Apollo 潜在客户挖掘工具:AI 驱动 B2B 销售线索生成与丰富
469 周安装
RAG工程师技能详解:检索增强生成系统架构与最佳实践
452 周安装
Typefully技能:跨平台社交媒体内容创建、安排与发布工具
455 周安装