API(Application Programming Interface,应用程序编程接口)是现代Web开发的基础,无论是前后端分离、移动端开发,还是第三方集成,都离不开API。REST(Representational State Transfer,表述性状态转移)是目前最流行的API设计风格,它基于HTTP协议,简单、灵活、易于扩展。

但是,很多开发者设计的RESTful API却混乱不堪:URL命名不规范、HTTP方法用错、状态码乱用、请求响应格式不统一、没有版本控制、没有文档……这样的API不仅难用,还难以维护,给前后端协作和第三方集成都带来了很多麻烦。

2016年,RESTful API已经成为Web开发的标准,前后端分离、移动端开发、微服务架构都离不开API。掌握RESTful API设计的最佳实践,是每个后端开发者的必备技能。今天就来详细讲解RESTful API设计的最佳实践,从URL命名到版本控制,帮助你设计出优雅、易用、可维护的API。

一、REST基础概念

1. 什么是REST

REST是一种软件架构风格,由Roy Fielding在2000年的博士论文中提出。REST的核心思想是:将所有事物都抽象为资源(Resource),每个资源有一个唯一的URI(统一资源标识符),通过HTTP方法(GET、POST、PUT、DELETE等)对资源进行操作。

REST的六个约束:

  • 客户端-服务器分离:客户端和服务器职责分离,通过接口通信
  • 无状态:每个请求都是独立的,服务器不保存客户端状态
  • 可缓存:响应可以被缓存,提高性能
  • 统一接口:使用统一的HTTP方法和状态码
  • 分层系统:客户端不知道是否直接连接到最终服务器
  • 按需代码(可选):服务器可以向客户端传输可执行代码

2. RESTful API的特点

  • 基于HTTP协议,使用HTTP方法表示操作
  • 使用URI表示资源
  • 使用JSON(或XML)作为数据交换格式
  • 使用HTTP状态码表示请求结果
  • 无状态,每个请求独立
  • 可缓存,提高性能

二、URL设计最佳实践

URL是API的入口,好的URL设计应该简洁、清晰、有意义、易于理解。

1. 使用名词,不用动词

REST的核心是资源,资源是名词,所以URL中应该使用名词,而不是动词。HTTP方法已经表示了操作(GET=查询,POST=创建,PUT=更新,DELETE=删除),不需要在URL中再加动词。

❌ 错误:
GET /getUsers          # 获取用户列表
GET /getUser?id=123    # 获取单个用户
POST /createUser       # 创建用户
POST /updateUser       # 更新用户
POST /deleteUser       # 删除用户

✅ 正确:
GET    /users          # 获取用户列表
GET    /users/123      # 获取单个用户
POST   /users          # 创建用户
PUT    /users/123      # 更新用户(全量更新)
PATCH  /users/123      # 更新用户(部分更新)
DELETE /users/123      # 删除用户

2. 使用复数名词

资源通常是集合,所以使用复数名词更符合直觉。

✅ 推荐:
/users          # 用户集合
/users/123      # 集合中的单个用户
/posts          # 文章集合
/posts/456      # 单篇文章
/comments       # 评论集合

虽然也有人使用单数(/user/123),但是复数更常见、更符合REST惯例。

3. 层级关系用斜杠

如果资源之间有层级关系(从属关系),用斜杠表示。

✅ 推荐:
/users/123/posts           # 用户123的所有文章
/users/123/posts/456       # 用户123的第456篇文章
/posts/456/comments        # 文章456的所有评论
/posts/456/comments/789    # 文章456的第789条评论
/categories/10/posts        # 分类10下的所有文章

但是,层级不要太深,一般不超过2-3层,否则URL会变得复杂难记。如果层级太深,可以考虑用查询参数代替。

4. 用连字符(-)分隔单词,不用下划线(_)

URL中多个单词之间用连字符(-)分隔,不用下划线(_)。这是因为连字符在大多数字体中更清晰可读,而且搜索引擎对连字符的处理更好。

✅ 推荐:
/user-profiles      # 用户资料
/post-categories    # 文章分类
/order-items        # 订单项

❌ 不推荐:
/user_profiles      # 下划线
/userProfiles       # 驼峰(URL不区分大小写,容易混淆)

5. 全部小写

URL路径全部小写,因为URL不区分大小写(域名部分不区分,路径部分取决于服务器,但惯例是小写)。全部小写可以避免混淆。

✅ 推荐:
/users/123/posts

❌ 不推荐:
/Users/123/Posts
/USERS/123/POSTS

6. 查询参数用于过滤、排序、分页

URL路径表示资源,查询参数(?key=value)用于过滤、排序、分页等操作。

✅ 推荐:
/users?status=active           # 过滤:只返回活跃用户
/users?role=admin              # 过滤:只返回管理员
/users?sort=created_at&order=desc  # 排序:按创建时间倒序
/users?page=2&per_page=20      # 分页:第2页,每页20条
/users?status=active&role=admin&page=1  # 组合使用

7. 不要在URL中加文件扩展名

不要在URL中加.json、.xml等文件扩展名。内容格式应该通过Accept请求头来协商,而不是URL扩展名。

✅ 推荐:
/users/123        # 通过Accept: application/json指定返回JSON

❌ 不推荐:
/users/123.json   # 扩展名
/users/123.xml    # 扩展名

三、HTTP方法最佳实践

HTTP方法表示对资源的操作,每个方法有明确的语义,不要混用。

方法语义幂等安全示例
GET查询资源GET /users, GET /users/123
POST创建资源POST /users
PUT全量更新资源PUT /users/123
PATCH部分更新资源PATCH /users/123
DELETE删除资源DELETE /users/123
  • 幂等:多次执行结果相同。GET、PUT、DELETE是幂等的,POST、PATCH不是。
  • 安全:不修改服务器资源。只有GET是安全的。

1. GET

用于查询资源,不应该修改服务器状态。GET请求应该是幂等的(多次调用结果相同)和安全的(不修改资源)。

GET /users              # 获取用户列表
GET /users/123          # 获取单个用户
GET /users/123/posts    # 获取用户的文章列表

GET请求的参数应该放在URL查询参数中,不要放在请求体中(虽然HTTP规范没有禁止,但很多服务器和代理不处理GET请求体)。

2. POST

用于创建资源。POST不是幂等的,多次调用会创建多个资源。

POST /users              # 创建用户
POST /users/123/posts    # 为用户123创建文章
POST /posts/456/comments # 为文章456创建评论

创建成功后,应该返回201 Created状态码,并在Location响应头中指向新创建的资源URI。

3. PUT

用于全量更新资源。PUT是幂等的,多次调用结果相同。PUT要求客户端发送完整的资源数据,服务器用客户端发送的数据完全替换原有资源。

PUT /users/123           # 全量更新用户123
# 请求体包含完整的用户数据:name, email, age, address等所有字段

注意:如果客户端只发送了部分字段,PUT会把未发送的字段设为null或默认值(全量替换)。如果只想更新部分字段,应该用PATCH。

4. PATCH

用于部分更新资源。PATCH不是幂等的(取决于实现)。PATCH只更新客户端发送的字段,未发送的字段保持不变。

PATCH /users/123         # 部分更新用户123
# 请求体只包含需要更新的字段:{"email": "new@example.com"}
# 其他字段(name, age等)保持不变

PATCH在2016年还不是所有框架都原生支持,但是越来越流行。如果框架不支持PATCH,可以用POST模拟,或者用PUT全量更新。

5. DELETE

用于删除资源。DELETE是幂等的,多次删除同一个资源结果相同(都是删除)。

DELETE /users/123        # 删除用户123
DELETE /posts/456/comments/789  # 删除评论789

删除成功后返回204 No Content(无响应体)或200 OK(带响应体说明删除结果)。

四、HTTP状态码最佳实践

HTTP状态码表示请求的结果,应该正确使用,不要所有请求都返回200 OK然后在响应体中用code字段表示状态。

1. 常用状态码

状态码含义使用场景
200 OK成功GET查询成功、PUT/PATCH更新成功、DELETE删除成功(带响应体)
201 Created已创建POST创建资源成功
204 No Content无内容DELETE删除成功(无响应体)、PUT/PATCH更新成功(无响应体)
301 Moved Permanently永久重定向资源URI永久变更
302 Found临时重定向资源URI临时变更
304 Not Modified未修改缓存验证,资源未变化
400 Bad Request请求错误请求参数错误、格式错误
401 Unauthorized未认证未登录、Token无效或过期
403 Forbidden禁止访问已登录但无权限
404 Not Found未找到资源不存在
405 Method Not Allowed方法不允许HTTP方法不支持
409 Conflict冲突资源冲突(如用户名已存在、版本冲突)
410 Gone已删除资源曾经存在但已永久删除
422 Unprocessable Entity无法处理请求格式正确但语义错误(如验证失败)
429 Too Many Requests请求过多限流,请求频率超限
500 Internal Server Error服务器内部错误服务器异常
502 Bad Gateway网关错误网关或代理服务器错误
503 Service Unavailable服务不可用服务器维护或过载
504 Gateway Timeout网关超时网关或代理超时

2. 状态码使用原则

  • 2xx:成功。200表示成功,201表示创建成功,204表示成功但无响应体。
  • 3xx:重定向。301永久重定向,302临时重定向,304缓存未修改。
  • 4xx:客户端错误。400参数错误,401未认证,403无权限,404不存在,422验证失败,429限流。
  • 5xx:服务器错误。500服务器内部错误,503服务不可用。

3. 不要这样做

❌ 错误:所有请求都返回200,在响应体中用code表示状态
HTTP/1.1 200 OK
{
    "code": 404,
    "message": "用户不存在",
    "data": null
}

✅ 正确:使用正确的HTTP状态码
HTTP/1.1 404 Not Found
{
    "error": "not_found",
    "message": "用户不存在"
}

五、请求和响应格式最佳实践

1. 使用JSON作为数据格式

2016年,JSON已经成为API数据交换的标准格式。JSON简洁、易读、跨语言支持好。除非有特殊需求(如XML用于SOAP),否则都应该用JSON。

请求头:
Content-Type: application/json
Accept: application/json

请求体:
{
    "name": "张三",
    "email": "zhangsan@example.com",
    "age": 25
}

响应体:
{
    "id": 123,
    "name": "张三",
    "email": "zhangsan@example.com",
    "age": 25,
    "created_at": "2016-06-17T10:00:00Z",
    "updated_at": "2016-06-17T10:00:00Z"
}

2. 统一的响应格式

API的响应格式应该统一,让客户端可以用一致的方式解析。

成功响应

{
    "data": {
        "id": 123,
        "name": "张三",
        "email": "zhangsan@example.com"
    }
}

列表响应(带分页)

{
    "data": [
        {"id": 1, "name": "张三"},
        {"id": 2, "name": "李四"}
    ],
    "meta": {
        "total": 100,
        "page": 1,
        "per_page": 20,
        "last_page": 5
    }
}

错误响应

{
    "error": {
        "code": "validation_error",
        "message": "请求参数验证失败",
        "details": [
            {
                "field": "email",
                "message": "邮箱格式不正确"
            },
            {
                "field": "age",
                "message": "年龄必须大于0"
            }
        ]
    }
}

统一的响应格式让客户端可以用一致的方式处理成功和错误,提高开发效率。

3. 字段命名规范

JSON字段命名应该统一,推荐使用snake_case(下划线命名),因为这是Ruby、Python、PHP等语言的惯例,也符合数据库字段命名。也可以用camelCase(驼峰命名),这是JavaScript的惯例。关键是要统一,不要混用。

✅ 推荐(snake_case):
{
    "user_name": "张三",
    "created_at": "2016-06-17T10:00:00Z",
    "is_active": true
}

✅ 也可以(camelCase):
{
    "userName": "张三",
    "createdAt": "2016-06-17T10:00:00Z",
    "isActive": true
}

❌ 不要混用:
{
    "user_name": "张三",      // snake_case
    "createdAt": "2016-06-17", // camelCase
    "IsActive": true           // PascalCase
}

4. 时间格式

时间应该使用ISO 8601格式,这是国际标准,跨语言支持好。

✅ 推荐:
"created_at": "2016-06-17T10:00:00Z"           // UTC时间
"created_at": "2016-06-17T18:00:00+08:00"      // 带时区

❌ 不推荐:
"created_at": "2016-06-17 10:00:00"             // 无时区,格式不标准
"created_at": 1466157600                          // Unix时间戳(不直观)
"created_at": "06/17/2016"                        // 格式不明确

5. 布尔值

布尔值应该用true/false,不要用1/0或"true"/"false"字符串。

✅ 推荐:
"is_active": true
"is_deleted": false

❌ 不推荐:
"is_active": 1
"is_active": "true"

六、分页、过滤、排序最佳实践

1. 分页

列表接口必须支持分页,避免一次返回太多数据导致性能问题。

✅ 推荐:
GET /users?page=1&per_page=20    # 第1页,每页20条
GET /users?offset=0&limit=20      # 偏移量方式(适合大数据量、需要深度分页的场景)

分页参数:

  • page:页码,从1开始
  • perpage / pagesize / limit:每页条数,默认20,最大100
  • offset:偏移量(offset = (page-1) * per_page)

响应中应该包含分页元数据:

{
    "data": [...],
    "meta": {
        "total": 100,        // 总条数
        "page": 1,           // 当前页
        "per_page": 20,      // 每页条数
        "last_page": 5       // 总页数
    }
}

对于大数据量的深度分页(如page=10000),offset方式性能很差(MySQL需要扫描前面所有行)。可以用游标分页(cursor-based pagination):

GET /users?cursor=12345&limit=20
# cursor是上一页最后一条记录的id,返回id > 12345的20条记录

2. 过滤

过滤参数直接放在查询参数中:

GET /users?status=active              # 按状态过滤
GET /users?role=admin                 # 按角色过滤
GET /users?status=active&role=admin   # 多条件组合
GET /posts?category_id=5              # 按分类过滤
GET /posts?tag=php                    # 按标签过滤
GET /posts?created_from=2016-01-01&created_to=2016-06-30  # 时间范围过滤

对于复杂的过滤(如范围、模糊搜索),可以用专门的参数名:

GET /users?q=张三                      # 模糊搜索
GET /users?min_age=18&max_age=60      # 年龄范围
GET /posts?date_from=2016-01-01&date_to=2016-06-30  # 日期范围

3. 排序

排序参数用sort和order(或direction):

GET /users?sort=created_at&order=desc   # 按创建时间倒序
GET /users?sort=name&order=asc           # 按名称正序
GET /posts?sort=views&order=desc         # 按浏览量倒序(热门文章)

多字段排序:

GET /users?sort=status,created_at&order=asc,desc
# 先按status正序,再按created_at倒序

默认排序:如果客户端没有指定排序,应该有一个合理的默认排序(如按created_at倒序,最新的在前)。

七、认证和授权最佳实践

1. 认证(Authentication)

认证是验证用户身份的过程。2016年主流的API认证方式:

  • API Key:在请求头或查询参数中传递API Key,简单但安全性较低,适合服务端到服务端的API。

`` GET /api/users X-API-Key: abc123def456 ``

  • OAuth 2.0:行业标准的授权框架,适合第三方应用授权。流程复杂但安全性高。

`` GET /api/users Authorization: Bearer <access_token> ``

  • JWT(JSON Web Token):2016年越来越流行的无状态认证方式。Token中包含用户信息,服务器不需要存储Session,适合分布式和微服务架构。

`` GET /api/users Authorization: Bearer <jwt_token> ``

  • Session/Cookie:传统的Web认证方式,有状态,适合传统Web应用,不适合纯API。

推荐:2016年,JWT是API认证的热门选择,无状态、适合分布式、跨域支持好。OAuth 2.0适合需要第三方授权的场景。

2. 授权(Authorization)

授权是验证用户是否有权限执行某个操作的过程。

  • 基于角色的访问控制(RBAC):给用户分配角色(如admin、editor、user),角色有权限,用户通过角色获得权限。简单易用,适合大多数应用。
  • 基于属性的访问控制(ABAC):根据用户、资源、环境的属性动态判断权限,灵活但复杂。

API中应该在服务端做权限校验,不要依赖前端隐藏按钮。每个API都应该检查当前用户是否有权限访问该资源或执行该操作。

3. 安全实践

  • 使用HTTPS,所有API通信加密
  • 不要在URL中传递敏感信息(如Token、密码),应该放在请求头或请求体中
  • Token应该有过期时间,支持刷新
  • 密码应该用bcrypt等安全算法哈希存储,不要明文存储
  • 对API进行限流,防止滥用和DDoS攻击
  • 对输入进行验证和过滤,防止SQL注入、XSS等攻击
  • 不要在错误信息中暴露敏感信息(如数据库错误、文件路径)

八、版本控制最佳实践

API会不断演进,版本控制是必须的。没有版本控制的API,一旦修改就可能破坏现有客户端。

1. 版本控制方式

方式一:URL中加版本号(推荐)

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

优点:简单直观,容易区分版本,方便同时维护多个版本。 缺点:URL变长,需要维护多个版本的路由。

方式二:请求头中加版本号

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

优点:URL干净,符合REST原则(内容协商)。 缺点:不直观,调试困难,很多开发者不熟悉。

方式三:查询参数加版本号

GET /api/users?version=1

优点:简单。 缺点:不优雅,容易被忽略,不推荐。

推荐:URL中加版本号(/api/v1/),简单直观,2016年大多数API都采用这种方式。

2. 版本控制原则

  • 一旦发布某个版本,就不应该再做破坏性修改(如删除字段、修改字段类型)
  • 新功能可以在新版本中添加,旧版本保持稳定
  • 应该有版本废弃计划,提前通知用户旧版本何时停止支持
  • 文档中应该说明每个版本的变更
  • 同时维护的版本不要太多(通常2-3个),否则维护成本高

3. 什么情况下需要新版本

  • 删除字段或端点
  • 修改字段名或类型
  • 修改API行为(如默认值、排序方式)
  • 修改认证方式
  • 重大架构调整

以下情况不需要新版本(向后兼容):

  • 添加新字段
  • 添加新端点
  • 添加新的可选参数
  • 优化性能
  • 修复bug(不改变预期行为)

九、错误处理最佳实践

1. 统一的错误响应格式

{
    "error": {
        "code": "validation_error",
        "message": "请求参数验证失败",
        "details": [
            {
                "field": "email",
                "message": "邮箱格式不正确"
            }
        ]
    }
}

字段说明:

  • code:错误码,字符串,程序可以根据错误码做不同处理
  • message:错误信息,人类可读,用于显示给用户
  • details:详细错误信息,如字段验证错误的具体字段和原因

2. 错误码设计

错误码应该有意义、有层次,不要用数字(如10001、10002),应该用字符串(如validationerror、notfound、unauthorized)。

常见错误码:

  • invalid_request:请求格式错误
  • validation_error:参数验证失败
  • unauthorized:未认证
  • forbidden:无权限
  • not_found:资源不存在
  • conflict:资源冲突
  • rate_limited:请求频率超限
  • internal_error:服务器内部错误

3. 错误信息原则

  • 错误信息应该清晰、有意义,帮助开发者快速定位问题
  • 不要暴露敏感信息(如数据库错误、堆栈跟踪、文件路径)
  • 生产环境返回简洁的错误信息,开发环境可以返回详细的调试信息
  • 错误信息应该本地化(根据Accept-Language返回对应语言)

十、文档最佳实践

API没有文档,就像产品没有说明书。好的API文档能大大提高开发效率,减少沟通成本。

1. 文档应该包含

  • API概述(简介、基础URL、认证方式)
  • 每个端点的详细说明(URL、HTTP方法、请求参数、响应格式、状态码、示例)
  • 认证和授权说明
  • 错误码说明
  • 版本变更日志
  • 常见问题(FAQ)
  • 代码示例(curl、JavaScript、Python、PHP等)

2. 文档工具

2016年流行的API文档工具:

  • Swagger(OpenAPI):行业标准,支持YAML/JSON定义API,自动生成交互式文档。2016年越来越流行。
  • API Blueprint:用Markdown写API文档,简洁易读。
  • RAML:RESTful API建模语言。
  • Postman:API测试工具,也可以生成文档。
  • GitBook / ReadMe:通用文档平台。

推荐:Swagger(OpenAPI),2016年已经成为API文档的事实标准,支持自动生成文档、客户端SDK、服务端框架等。

3. 文档原则

  • 文档应该和代码同步更新,不要文档和实际API不一致
  • 文档应该有示例,示例比文字说明更直观
  • 文档应该易于搜索和导航
  • 文档应该有版本,对应API的版本
  • 鼓励开发者反馈文档问题,持续改进

十一、其他最佳实践

1. 限流(Rate Limiting)

对API进行限流,防止滥用和DDoS攻击。可以按IP、用户、API Key限流。

限流信息应该在响应头中返回:

X-RateLimit-Limit: 1000       # 每小时限制1000次
X-RateLimit-Remaining: 995    # 剩余995次
X-RateLimit-Reset: 1466157600 # 重置时间(Unix时间戳)

超过限流返回429 Too Many Requests。

2. 缓存

对不常变化的数据进行缓存,提高性能,减轻服务器压力。可以用HTTP缓存头(Cache-Control、ETag、Last-Modified),也可以用Redis等服务端缓存。

响应头:
Cache-Control: max-age=3600    # 缓存1小时
ETag: "abc123"                  # 资源版本标识
Last-Modified: Wed, 17 Jun 2016 10:00:00 GMT

3. CORS(跨域资源共享)

如果API需要被浏览器端的JavaScript调用(前后端分离),需要配置CORS。

响应头:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600

不要用Access-Control-Allow-Origin: *(允许所有来源),除非是公开API。应该指定具体的域名。

4. 请求ID

给每个请求分配一个唯一的请求ID,方便日志追踪和问题排查。

响应头:
X-Request-Id: a1b2c3d4e5f6

客户端也可以发送X-Request-Id,服务端记录在日志中,出现问题时可以通过请求ID快速定位。

5. 健康检查端点

提供一个健康检查端点,用于监控和负载均衡。

GET /health
响应:200 OK {"status": "ok"}

总结

RESTful API设计是一门艺术,也是一门科学。好的API设计应该简洁、清晰、有意义、易于使用、易于维护。本文从URL设计、HTTP方法、状态码、请求响应格式、分页过滤排序、认证授权、版本控制、错误处理、文档等方面,讲解了RESTful API设计的最佳实践。

核心原则总结:

  1. URL用名词复数,不用动词,HTTP方法表示操作
  2. 正确使用HTTP状态码,不要所有请求都返回200
  3. 使用JSON格式,统一请求响应格式
  4. 列表接口支持分页、过滤、排序
  5. 使用HTTPS和安全的认证方式(JWT/OAuth 2.0)
  6. API必须有版本控制,URL中加版本号(/api/v1/)
  7. 统一的错误处理,清晰的错误码和错误信息
  8. 完善的API文档,和代码同步更新
  9. 限流、缓存、CORS、请求ID等工程实践
  10. 向后兼容,非必要不做破坏性修改

API是给开发者用的,好的API能让开发者用得舒心、用得高效。希望本文的最佳实践能帮助你设计出优雅、易用、可维护的RESTful API。

最后,记住:API设计没有绝对的标准答案,关键是要统一、一致、有文档。只要你的团队达成共识,严格遵守约定,就是好的API设计。