RESTful API 设计:构建优雅的 Web 接口
在当今的软件开发中,API(应用程序接口)已经成为连接不同系统和服务的桥梁。RESTful API 因其简单性、灵活性和可扩展性,成为了最流行的 API 设计风格之一。
本文将深入探讨如何设计优秀的 RESTful API,让你的接口既符合规范,又易于使用。
什么是 REST?
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中首次提出。REST 的核心原则包括:
- 资源(Resource):一切皆资源,用 URI 标识
- 表述(Representation):资源可以有多种表述形式(JSON、XML 等)
- 状态转移(State Transfer):通过 HTTP 方法操作资源
- 无状态(Stateless):每个请求包含所有必要信息
- 统一接口(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 需要遵循以下原则:
- 使用名词表示资源,动词由 HTTP 方法承担
- 正确使用 HTTP 方法:GET、POST、PUT、PATCH、DELETE
- 返回合适的状态码,让客户端了解请求结果
- 设计清晰的 URL 结构,使用层级表示关系
- 提供灵活的查询参数,支持过滤、排序、分页
- 统一的响应格式,包含数据、元数据和链接
- 完善的错误处理,提供有意义的错误信息
- 编写详细的文档,使用 OpenAPI 规范
记住:好的 API 设计能让开发者事半功倍,而糟糕的设计会让使用者痛苦不堪。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。