RESTful API 设计:构建优雅的 Web 接口

学习 RESTful API 设计原则和最佳实践,构建清晰、一致、易用的 Web API

RESTful API 设计:构建优雅的 Web 接口

在当今的软件开发中,API(应用程序接口)已经成为连接不同系统和服务的桥梁。RESTful API 因其简单性、灵活性和可扩展性,成为了最流行的 API 设计风格之一。

本文将深入探讨如何设计优秀的 RESTful API,让你的接口既符合规范,又易于使用。

什么是 REST?

REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中首次提出。REST 的核心原则包括:

  1. 资源(Resource):一切皆资源,用 URI 标识
  2. 表述(Representation):资源可以有多种表述形式(JSON、XML 等)
  3. 状态转移(State Transfer):通过 HTTP 方法操作资源
  4. 无状态(Stateless):每个请求包含所有必要信息
  5. 统一接口(Uniform Interface):使用标准的 HTTP 方法

URL 设计原则

使用名词而非动词

❌ 错误示例:
GET /getUser?id=123
POST /createUser
DELETE /deleteUser/123

✅ 正确示例:
GET /users/123
POST /users
DELETE /users/123

URL 应该表示资源,操作由 HTTP 方法决定。

使用复数名词

❌ 错误:/user/123
✅ 正确:/users/123

保持一致性,即使某个资源只有一个实例。

使用层级结构表示关系

GET /users/123/orders          # 获取用户 123 的所有订单
GET /users/123/orders/456      # 获取用户 123 的订单 456
GET /users/123/profile         # 获取用户 123 的个人资料

但层级不宜过深,通常不超过 2 层。

使用小写字母和连字符

❌ 错误:/UserProfiles
❌ 错误:/user_profiles
✅ 正确:/user-profiles

版本号管理

方式 1:URL 版本(推荐)
GET /v1/users
GET /v2/users

方式 2:请求头版本
GET /users
Header: Accept: application/vnd.myapi.v2+json

HTTP 方法的正确使用

GET - 获取资源

// 获取资源列表
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: 1, Name: "Alice"},
        {ID: 2, Name: "Bob"},
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

// 获取单个资源
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := extractID(r.URL.Path)
    user, err := findUser(id)
    if err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// 路由注册
mux.HandleFunc("/users", getUsersHandler)
mux.HandleFunc("/users/", getUserHandler)

特点:

  • 幂等:多次请求结果相同
  • 安全:不修改服务器状态
  • 可缓存

POST - 创建资源

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 创建用户
    id, err := createUser(&user)
    if err != nil {
        http.Error(w, "Failed to create user", http.StatusInternalServerError)
        return
    }
    
    // 返回 201 Created 和资源位置
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Location", fmt.Sprintf("/users/%d", id))
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

特点:

  • 非幂等:每次请求都会创建新资源
  • 返回 201 Created 状态码
  • 包含 Location 头指向新资源

PUT - 完整更新资源

func updateUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPut {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    id := extractID(r.URL.Path)
    
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 完整更新
    if err := replaceUser(id, &user); err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}

特点:

  • 幂等:多次请求结果相同
  • 需要提供资源的完整数据
  • 通常返回 204 No Content

PATCH - 部分更新资源

func patchUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPatch {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    id := extractID(r.URL.Path)
    
    // 解析部分更新字段
    var updates map[string]interface{}
    if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 部分更新
    if err := updateUser(id, updates); err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}

特点:

  • 非幂等(理论上)
  • 只需要提供要修改的字段
  • 适合大量字段的资源

DELETE - 删除资源

func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodDelete {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    id := extractID(r.URL.Path)
    
    if err := deleteUser(id); err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}

特点:

  • 幂等:删除已删除的资源不算错误
  • 返回 204 No Content

状态码的正确使用

成功状态码

200 OK          - 请求成功(GET、PUT、PATCH、DELETE)
201 Created     - 资源创建成功(POST)
204 No Content  - 请求成功但无返回内容(PUT、PATCH、DELETE)

客户端错误

400 Bad Request          - 请求格式错误
401 Unauthorized         - 未认证
403 Forbidden            - 无权限
404 Not Found            - 资源不存在
405 Method Not Allowed   - 方法不允许
409 Conflict             - 资源冲突
422 Unprocessable Entity - 请求格式正确但语义错误

服务器错误

500 Internal Server Error - 服务器内部错误
503 Service Unavailable   - 服务不可用

查询参数设计

过滤

GET /users?status=active
GET /products?category=electronics&brand=apple

排序

GET /users?sort=created_at
GET /users?sort=-created_at  # 降序
GET /users?sort=name,-created_at  # 多字段排序

分页

方式 1:offset + limit
GET /users?offset=20&limit=10

方式 2:page + per_page
GET /users?page=2&per_page=10

方式 3:cursor(推荐用于大数据集)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=10

字段选择

GET /users?fields=id,name,email

只返回指定字段,减少数据传输。

响应格式设计

统一响应结构

{
  "data": { ... },
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 100
  },
  "links": {
    "self": "/users?page=1",
    "next": "/users?page=2",
    "prev": null
  }
}

错误响应

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Must be greater than 0"
      }
    ]
  }
}

实战:完整的用户 API

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserStore struct {
    mu     sync.RWMutex
    users  map[int]*User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]*User),
        nextID: 1,
    }
}

var store = NewUserStore()

// Response 统一响应结构
type Response struct {
    Data  interface{} `json:"data,omitempty"`
    Error *ErrorInfo  `json:"error,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

type ErrorInfo struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

type Meta struct {
    Total int `json:"total,omitempty"`
}

func respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(Response{Data: data})
}

func respondError(w http.ResponseWriter, status int, code, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(Response{
        Error: &ErrorInfo{Code: code, Message: message},
    })
}

// GET /users - 获取用户列表
func listUsers(w http.ResponseWriter, r *http.Request) {
    store.mu.RLock()
    defer store.mu.RUnlock()
    
    users := make([]*User, 0, len(store.users))
    for _, user := range store.users {
        users = append(users, user)
    }
    
    respondJSON(w, http.StatusOK, users)
}

// GET /users/:id - 获取单个用户
func getUser(w http.ResponseWriter, r *http.Request) {
    idStr := strings.TrimPrefix(r.URL.Path, "/users/")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
        return
    }
    
    store.mu.RLock()
    defer store.mu.RUnlock()
    
    user, exists := store.users[id]
    if !exists {
        respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
        return
    }
    
    respondJSON(w, http.StatusOK, user)
}

// POST /users - 创建用户
func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        respondError(w, http.StatusBadRequest, "INVALID_BODY", "Invalid request body")
        return
    }
    
    // 验证
    if user.Name == "" {
        respondError(w, http.StatusUnprocessableEntity, "VALIDATION_ERROR", "Name is required")
        return
    }
    
    store.mu.Lock()
    user.ID = store.nextID
    store.users[user.ID] = &user
    store.nextID++
    store.mu.Unlock()
    
    w.Header().Set("Location", fmt.Sprintf("/users/%d", user.ID))
    respondJSON(w, http.StatusCreated, user)
}

// PUT /users/:id - 更新用户
func updateUser(w http.ResponseWriter, r *http.Request) {
    idStr := strings.TrimPrefix(r.URL.Path, "/users/")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
        return
    }
    
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        respondError(w, http.StatusBadRequest, "INVALID_BODY", "Invalid request body")
        return
    }
    
    store.mu.Lock()
    defer store.mu.Unlock()
    
    if _, exists := store.users[id]; !exists {
        respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
        return
    }
    
    user.ID = id
    store.users[id] = &user
    w.WriteHeader(http.StatusNoContent)
}

// DELETE /users/:id - 删除用户
func deleteUser(w http.ResponseWriter, r *http.Request) {
    idStr := strings.TrimPrefix(r.URL.Path, "/users/")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID")
        return
    }
    
    store.mu.Lock()
    defer store.mu.Unlock()
    
    if _, exists := store.users[id]; !exists {
        respondError(w, http.StatusNotFound, "NOT_FOUND", "User not found")
        return
    }
    
    delete(store.users, id)
    w.WriteHeader(http.StatusNoContent)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path
    
    if path == "/users" {
        switch r.Method {
        case http.MethodGet:
            listUsers(w, r)
        case http.MethodPost:
            createUser(w, r)
        default:
            respondError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
        }
        return
    }
    
    // /users/:id
    switch r.Method {
    case http.MethodGet:
        getUser(w, r)
    case http.MethodPut:
        updateUser(w, r)
    case http.MethodDelete:
        deleteUser(w, r)
    default:
        respondError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", usersHandler)
    mux.HandleFunc("/users/", usersHandler)
    
    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", mux)
}

API 文档

使用 Swagger/OpenAPI 规范编写 API 文档:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get all users
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
    post:
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '201':
          description: User created
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
      required:
        - name
        - email

总结

设计优秀的 RESTful API 需要遵循以下原则:

  1. 使用名词表示资源,动词由 HTTP 方法承担
  2. 正确使用 HTTP 方法:GET、POST、PUT、PATCH、DELETE
  3. 返回合适的状态码,让客户端了解请求结果
  4. 设计清晰的 URL 结构,使用层级表示关系
  5. 提供灵活的查询参数,支持过滤、排序、分页
  6. 统一的响应格式,包含数据、元数据和链接
  7. 完善的错误处理,提供有意义的错误信息
  8. 编写详细的文档,使用 OpenAPI 规范

记住:好的 API 设计能让开发者事半功倍,而糟糕的设计会让使用者痛苦不堪。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页