SuperAPI AI Directory

api-design

Coding & development

View repository

Updated 10-16 07:22

About this skill

API Design Patterns

Specifications and best practices for designing consistent and developer-friendly REST APIs.

When to Enable

  • Designing new API endpoints
  • Reviewing existing API contracts
  • Adding pagination, filtering, or sorting functionality
  • Implementing error handling for the API
  • Planning an API versioning strategy
  • Building public or partner-facing APIs

Resource Design

URL Structure

# 资源是名词、复数、小写、短横线命名 (kebab-case)
GET    /api/v1/users
GET    /api/v1/users/:id
POST   /api/v1/users
PUT    /api/v1/users/:id
PATCH  /api/v1/users/:id
DELETE /api/v1/users/:id

# 关系子资源
GET    /api/v1/users/:id/orders
POST   /api/v1/users/:id/orders

# 不属于 CRUD 的操作(谨慎使用动词)
POST   /api/v1/orders/:id/cancel
POST   /api/v1/auth/login
POST   /api/v1/auth/refresh

Naming Rules

# 正确示例 (GOOD)
/api/v1/team-members          # 多单词资源使用 kebab-case
/api/v1/orders?status=active  # 使用查询参数进行过滤
/api/v1/users/123/orders      # 嵌套资源表示所属关系

# 错误示例 (BAD)
/api/v1/getUsers              # URL 中包含动词
/api/v1/user                  # 使用单数(应使用复数)
/api/v1/team_members          # URL 中使用 snake_case
/api/v1/users/123/getOrders   # 嵌套资源中包含动词

HTTP Methods and Status Codes

Method Semantics

Method Idempotent Safe Purpose
GET Yes Yes Retrieve resources
POST No No Create resources, trigger operations
PUT Yes No Fully replace resources
PATCH No* No Partially update resources
DELETE Yes No Delete resources

*If implemented properly, PATCH can also be made idempotent.

Status Code Reference

# 成功 (Success)
200 OK                    — GET, PUT, PATCH(包含响应正文)
201 Created               — POST(需包含 Location 标头)
204 No Content            — DELETE, PUT(无响应正文)

# 客户端错误 (Client Errors)
400 Bad Request           — 验证失败、JSON 格式错误
401 Unauthorized          — 缺失或无效的身份验证
403 Forbidden             — 已身份验证但无权访问
404 Not Found             — 资源不存在
409 Conflict              — 重复条目、状态冲突
422 Unprocessable Entity  — 语义无效(JSON 合法,但数据有误)
429 Too Many Requests     — 超出速率限制

# 服务器错误 (Server Errors)
500 Internal Server Error — 意外故障(绝不要暴露详细信息)
502 Bad Gateway           — 上游服务故障
503 Service Unavailable   — 临时过载,包含 Retry-After 标头

Common Errors

# 错误:所有响应都返回 200
{ "status": 200, "success": false, "error": "Not found" }

# 正确:语义化使用 HTTP 状态码
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "User not found" } }

# 错误:验证错误返回 500
# 正确:返回 400 或 422,并附带字段级详细信息

# 错误:创建资源返回 200
# 正确:返回 201 并在 Location 标头中包含路径
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123

Response Format

Success Response

{
  "data": {
    "id": "abc-123",
    "email": "alice@example.com",
    "name": "Alice",
    "created_at": "2025-01-15T10:30:00Z"
  }
}

Collection Response (with Pagination)

{
  "data": [
    { "id": "abc-123", "name": "Alice" },
    { "id": "def-456", "name": "Bob" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "per_page": 20,
    "total_pages": 8
  },
  "links": {
    "self": "/api/v1/users?page=1&per_page=20",
    "next": "/api/v1/users?page=2&per_page=20",
    "last": "/api/v1/users?page=8&per_page=20"
  }
}

Error Response

{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "code": "invalid_format"
      },
      {
        "field": "age",
        "message": "Must be between 0 and 150",
        "code": "out_of_range"
      }
    ]
  }
}

Response Envelope Variants

// 选项 A:带数据包装器的封装(推荐用于公开 API)
interface ApiResponse<T> {
  data: T;
  meta?: PaginationMeta;
  links?: PaginationLinks;
}

interface ApiError {
  error: {
    code: string;
    message: string;
    details?: FieldError[];
  };
}

// 选项 B:扁平化响应(较简单,常用于内部 API)
// 成功:直接返回资源
// 错误:返回错误对象
// 通过 HTTP 状态码进行区分

Pagination

Offset-Based - Simple

GET /api/v1/users?page=2&per_page=20

# 实现
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;

Pros: Easy to implement, supports "jump to page N". Cons: Poor performance with large offsets (OFFSET 100000), and may be inconsistent during concurrent inserts.

Cursor-Based - Scalable

GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20

# 实现
SELECT * FROM users
WHERE id > :cursor_id
ORDER BY id ASC
LIMIT 21;  -- 多获取一个以确定是否有下一页 (has_next)
{
  "data": [...],
  "meta": {
    "has_next": true,
    "next_cursor": "eyJpZCI6MTQzfQ"
  }
}

Pros: Consistent performance regardless of position; stable during concurrent inserts. Cons: Cannot jump to an arbitrary page number; cursors are opaque.

When to Use Which

Use Case Pagination Type
Admin backends, small datasets (<10K) Offset
Infinite scroll, feeds, large datasets Cursor
Public APIs Use Cursor by default, with optional Offset
Search results Offset (users usually expect to see page numbers)

Filtering, Sorting, and Search

Filtering

# 简单等值匹配
GET /api/v1/orders?status=active&customer_id=abc-123

# 比较运算符(使用方括号标记)
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01

# 多个值(逗号分隔)
GET /api/v1/products?category=electronics,clothing

# 嵌套字段(点标记法)
GET /api/v1/orders?customer.country=US

Sorting

# 单个字段(前缀 - 表示降序)
GET /api/v1/products?sort=-created_at

# 多个字段(逗号分隔)
GET /api/v1/products?sort=-featured,price,-created_at

Full-Text Search

# 搜索查询参数
GET /api/v1/products?q=wireless+headphones

# 特定字段搜索
GET /api/v1/users?email=alice

Sparse Fieldsets

# 仅返回指定的字段(减少数据传输量)
GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name

Authentication and Authorization

Token-Based Auth

# Authorization 标头中的 Bearer 令牌
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# API 密钥(用于服务器对服务器通信)
GET /api/v1/data
X-API-Key: sk_live_abc123

Authorization Patterns

// 资源级别:检查所有权
app.get("/api/v1/orders/:id", async (req, res) => {
  const order = await Order.findById(req.params.id);
  if (!order) return res.status(404).json({ error: { code: "not_found" } });
  if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
  return res.json({ data: order });
});

// 基于角色:检查权限
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
  await User.delete(req.params.id);
  return res.status(204).send();
});

Rate Limiting

Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

# 当超出限制时
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Try again in 60 seconds."
  }
}

Rate Limit Tiers

Tier Limit Window Use Case
Anonymous 30/min Per IP Public endpoints
Authenticated 100/min Per user Standard API access
Premium 1000/min Per API key Paid API plans
Internal 10000/min Per service Service-to-service calls

Versioning

URL path versioning (recommended)

/api/v1/users
/api/v2/users

Pros: Explicit, easy to route, cacheable. Cons: URLs change between versions.

Header versioning

GET /api/users
Accept: application/vnd.myapp.v2+json

Pros: Clean URLs. Cons: Harder to test, easy to forget.

Versioning strategies

1. 从 /api/v1/ 开始 —— 在确实需要之前不要进行版本控制
2. 最多维护 2 个活跃版本(当前版本 + 上一个版本)
3. 弃用时间线:
   - 宣布弃用(公开 API 通常提前 6 个月通知)
   - 添加 Sunset 标头:Sunset: Sat, 01 Jan 2026 00:00:00 GMT
   - 在 Sunset 日期后返回 410 Gone
4. 非破坏性更改不需要新版本:
   - 在响应中添加新字段
   - 添加新的可选查询参数
   - 添加新的端点
5. 破坏性更改需要新版本:
   - 删除或重命名字段
   - 更改字段类型
   - 更改 URL 结构
   - 更改身份验证方法

Implementation Patterns

TypeScript (Next.js API Route)

import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export async function POST(req: NextRequest) {
  const body = await req.json();
  const parsed = createUserSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json({
      error: {
        code: "validation_error",
        message: "Request validation failed",
        details: parsed.error.issues.map(i => ({
          field: i.path.join("."),
          message: i.message,
          code: i.code,
        })),
      },
    }, { status: 422 });
  }

  const user = await createUser(parsed.data);

  return NextResponse.json(
    { data: user },
    {
      status: 201,
      headers: { Location: `/api/v1/users/${user.id}` },
    },
  );
}

Python (Django REST Framework)

from rest_framework import serializers, viewsets, status
from rest_framework.response import Response

class CreateUserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    name = serializers.CharField(max_length=100)

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "email", "name", "created_at"]

class UserViewSet(viewsets.ModelViewSet):
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == "create":
            return CreateUserSerializer
        return UserSerializer

    def create(self, request):
        serializer = CreateUserSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = UserService.create(**serializer.validated_data)
        return Response(
            {"data": UserSerializer(user).data},
            status=status.HTTP_201_CREATED,
            headers={"Location": f"/api/v1/users/{user.id}"},
        )

Go (net/http)

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
        return
    }

    if err := req.Validate(); err != nil {
        writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
        return
    }

    user, err := h.service.Create(r.Context(), req)
    if err != nil {
        switch {
        case errors.Is(err, domain.ErrEmailTaken):
            writeError(w, http.StatusConflict, "email_taken", "Email already registered")
        default:
            writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
        }
        return
    }

    w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
    writeJSON(w, http.StatusCreated, map[string]any{"data": user})
}

API Design Checklist

Before shipping a new endpoint:

  • Resource URLs follow naming conventions (plural, kebab-case, no verbs)
  • Correct HTTP methods are used (GET for reads, POST for creates, etc.)
  • Appropriate status codes are returned (don't return 200 for everything)
  • Input is validated using a schema (Zod, Pydantic, Bean Validation)
  • Error responses follow the standard format with code and message
  • Pagination is implemented for list endpoints (cursor or offset)
  • Authentication is required (or explicitly marked as public)
  • Authorization is checked (users can only access their own resources)
  • Rate limiting is configured
  • Responses do not leak internal details (stack traces, SQL errors)
  • Naming is consistent with existing endpoints (camelCase vs snake_case)
  • Documented (OpenAPI/Swagger spec updated)