在Web开发中,API(Application Programming Interface,应用程序编程接口)是前后端交互的桥梁,也是不同系统之间通信的纽带。一个设计良好的API,能够提高开发效率,降低维护成本,提升系统的可扩展性和可维护性。

REST(Representational State Transfer,表述性状态转移)是目前最流行的API设计风格,它基于HTTP协议,利用HTTP的方法(GET、POST、PUT、DELETE等)和状态码来表示对资源的操作。RESTful API具有简单、直观、可缓存、无状态等优点,被广泛应用于Web服务和移动应用后端。

今天就来分享RESTful API设计的最佳实践,帮助大家打造优雅、易用、可维护的接口。

REST简介

1. 什么是REST

REST是由Roy Fielding在2000年的博士论文中提出的一种软件架构风格,它不是一个标准,而是一组设计原则和约束条件。RESTful API就是遵循REST原则设计的API。

REST的核心概念是"资源"(Resource),网络上的所有事物都可以被抽象为资源,如用户、文章、评论、订单等。每个资源都有一个唯一的URI(统一资源标识符)来标识,通过HTTP方法对资源进行操作。

REST的六个约束条件:

  • 客户端-服务器分离:客户端和服务器职责分离,客户端负责用户界面,服务器负责数据存储和业务逻辑,两者可以独立演进
  • 无状态:服务器不保存客户端的状态,每个请求都必须包含所有必要的信息,服务器可以独立处理每个请求
  • 可缓存:响应可以被缓存,减少服务器负载,提高性能
  • 统一接口:使用统一的接口设计,包括资源标识、通过表示操作资源、自描述消息、超媒体作为应用状态引擎
  • 分层系统:系统可以分层,客户端无法直接感知中间层,中间层可以提供负载均衡、缓存、安全等功能
  • 按需代码(可选):服务器可以向客户端传输可执行代码,如JavaScript,扩展客户端功能

2. REST的核心原则

  • 一切皆资源:网络上的所有事物都可以被抽象为资源,每个资源有唯一的URI
  • 通过HTTP方法操作资源:GET获取资源,POST创建资源,PUT更新资源(全量),PATCH更新资源(部分),DELETE删除资源
  • 无状态:每个请求都是独立的,服务器不保存客户端状态
  • 使用HTTP状态码:使用标准的HTTP状态码表示请求结果,如200成功、404未找到、500服务器错误
  • 资源表示:资源可以有多种表示形式,如JSON、XML、HTML等,通过Content-Type和Accept协商

3. RESTful API的优点

  • 简单直观:基于HTTP协议,使用标准的HTTP方法和状态码,学习成本低,易于理解
  • 无状态:服务器不需要保存客户端状态,易于扩展和负载均衡
  • 可缓存:GET请求可以被缓存,提高性能,减少服务器负载
  • 语言无关:RESTful API基于HTTP和JSON,任何语言都可以调用,跨平台性好
  • 前后端分离:RESTful API天然支持前后端分离架构,前端和后端可以独立开发和部署
  • 可演进性:通过版本控制和超媒体,API可以平滑演进,不破坏已有客户端

URI设计

URI(统一资源标识符)是RESTful API的入口,好的URI设计应该直观、可读、一致、可预测。

1. 使用名词,不用动词

URI应该使用名词来表示资源,而不是动词。HTTP方法已经表示了操作(GET获取、POST创建、PUT更新、DELETE删除),URI中不需要再包含动词。

# 好的设计(名词)
GET    /users          # 获取用户列表
GET    /users/123      # 获取ID为123的用户
POST   /users          # 创建用户
PUT    /users/123      # 更新ID为123的用户
DELETE /users/123      # 删除ID为123的用户

# 不好的设计(动词)
GET    /getUsers       # 获取用户列表
GET    /getUser/123    # 获取ID为123的用户
POST   /createUser     # 创建用户
POST   /updateUser/123 # 更新ID为123的用户
POST   /deleteUser/123 # 删除ID为123的用户

2. 使用复数名词

资源集合通常使用复数名词,单个资源在集合后加ID。

# 好的设计(复数)
/articles          # 文章集合
/articles/123      # ID为123的文章
/articles/123/comments  # ID为123的文章的评论集合

# 不推荐(单数)
/article           # 文章集合
/article/123       # ID为123的文章

当然,也有一些特殊情况,如某些资源天然是单数(如用户的个人资料/profile),或者某些动作无法用资源表示(如登录/login、搜索/search),这些可以例外处理。

3. 层级关系

如果资源之间有从属关系,URI应该体现这种层级关系。

/users/123/articles          # ID为123的用户的所有文章
/users/123/articles/456      # ID为123的用户的ID为456的文章
/articles/456/comments       # ID为456的文章的所有评论
/articles/456/comments/789   # ID为456的文章的ID为789的评论
/categories/12/articles      # ID为12的分类下的所有文章

但是,层级不要太深,一般不超过2-3层,否则URI会变得复杂和难读。如果层级太深,可以考虑扁平化设计,或者通过查询参数过滤。

# 层级太深(不推荐)
/users/123/articles/456/comments/789/replies/012

# 扁平化(推荐)
/comments/789/replies        # 通过评论ID获取回复,评论和文章的关系在数据中体现

4. 使用连字符,不用下划线

URI中多个单词之间使用连字符(-)分隔,而不是下划线(_)。因为在某些字体中,下划线可能被链接的下划线遮挡,而连字符更清晰可读。

# 好的设计(连字符)
/user-profiles
/article-categories
/order-items

# 不好的设计(下划线)
/user_profiles
/article_categories
/order_items

5. 全小写

URI中全部使用小写字母,因为URI是大小写敏感的(除了协议和主机名),统一使用小写可以避免混淆和重复。

# 好的设计(全小写)
/users
/articles
/article-categories

# 不好的设计(大小写混合)
/Users
/Articles
/ArticleCategories

6. 文件扩展名

URI中不应该包含文件扩展名(如.json、.xml),应该通过HTTP头的Content-Type和Accept来协商表示格式。

# 好的设计(无扩展名)
GET /users/123
Accept: application/json

# 不好的设计(带扩展名)
GET /users/123.json
GET /users/123.xml

HTTP方法

RESTful API使用标准的HTTP方法来表示对资源的操作,每个方法有明确的语义和幂等性。

1. 常用HTTP方法

方法语义幂等安全典型用途
GET获取资源查询资源列表或单个资源
POST创建资源创建新资源
PUT全量更新资源完整更新已有资源
PATCH部分更新资源部分更新已有资源
DELETE删除资源删除资源
HEAD获取资源头信息检查资源是否存在,获取元数据
OPTIONS获取资源支持的方法跨域预检,查询支持的操作

2. 幂等性和安全性

  • 安全方法:GET、HEAD、OPTIONS,不会修改服务器资源,只是获取信息
  • 幂等方法:GET、PUT、DELETE、HEAD、OPTIONS,多次执行结果相同,不会产生副作用
  • 非幂等方法:POST、PATCH,多次执行可能产生不同结果或副作用

理解幂等性很重要,在网络不稳定的情况下,幂等方法可以安全重试,而非幂等方法需要谨慎处理重复提交。

3. 方法使用示例

# 文章资源的CRUD操作
GET    /articles          # 获取文章列表
GET    /articles/123      # 获取ID为123的文章
POST   /articles          # 创建新文章
PUT    /articles/123      # 全量更新ID为123的文章(替换整个文章)
PATCH  /articles/123      # 部分更新ID为123的文章(只更新某些字段)
DELETE /articles/123      # 删除ID为123的文章

# 子资源操作
GET    /articles/123/comments    # 获取文章的评论列表
POST   /articles/123/comments    # 为文章添加评论
DELETE /articles/123/comments/456 # 删除文章的指定评论

# 特殊操作(无法用资源表示的动作)
POST   /auth/login          # 登录
POST   /auth/logout         # 登出
POST   /users/123/activate  # 激活用户
POST   /articles/123/publish # 发布文章

对于特殊操作,如果无法用标准的CRUD表示,可以在URI后加动作动词,用POST方法执行。但是要尽量少用,优先考虑将动作抽象为资源。

HTTP状态码

RESTful API应该使用标准的HTTP状态码来表示请求结果,而不是所有请求都返回200然后在响应体中用自定义错误码。

1. 常用状态码

状态码含义典型用途
200 OK请求成功GET、PUT、PATCH成功
201 Created资源创建成功POST创建资源成功
204 No Content操作成功,无返回内容DELETE成功,PUT/PATCH无返回
301 Moved Permanently永久重定向资源URI变更
304 Not Modified资源未修改缓存命中,资源未变化
400 Bad Request请求参数错误参数缺失、格式错误、验证失败
401 Unauthorized未认证未登录或token无效
403 Forbidden无权限已登录但无权访问该资源
404 Not Found资源不存在请求的资源不存在
405 Method Not Allowed方法不允许资源不支持该HTTP方法
409 Conflict冲突资源冲突,如重复创建、版本冲突
410 Gone资源已删除资源曾经存在但已永久删除
415 Unsupported Media Type不支持的媒体类型请求的Content-Type不支持
422 Unprocessable Entity无法处理的实体参数格式正确但语义错误,如验证失败
429 Too Many Requests请求过多限流,请求频率超限
500 Internal Server Error服务器内部错误服务器异常,如代码bug、数据库错误
502 Bad Gateway网关错误反向代理或网关错误
503 Service Unavailable服务不可用服务器过载或维护中
504 Gateway Timeout网关超时网关或代理超时

2. 状态码使用原则

  • 精确使用状态码:根据实际情况选择最合适的状态码,不要所有错误都返回400或500
  • 不要用200表示错误:错误应该用4xx或5xx状态码,不要返回200然后在响应体中写错误码
  • 201用于创建成功:POST创建资源成功应该返回201,并在Location头中指向新资源的URI
  • 204用于无返回内容:DELETE成功或PUT/PATCH不需要返回内容时用204
  • 401用于未认证:用户未登录或token无效时用401
  • 403用于无权限:用户已登录但无权操作时用403
  • 404用于资源不存在:请求的资源不存在时用404
  • 422用于验证失败:参数格式正确但内容验证失败时用422(比400更精确)
  • 500用于服务器错误:服务器内部异常时用500,同时记录日志便于排查

请求和响应格式

1. 请求格式

  • 使用JSON作为请求体格式(Content-Type: application/json)
  • POST和PUT请求的请求体应该包含资源的字段
  • PATCH请求可以只包含需要更新的字段
  • 查询参数用于过滤、排序、分页、搜索等
# 创建用户的请求
POST /users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "password": "secret123",
  "age": 25
}

# 更新用户的请求(全量)
PUT /users/123
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 26
}

# 更新用户的请求(部分)
PATCH /users/123
Content-Type: application/json

{
  "age": 26
}

# 查询参数示例
GET /articles?category=tech&tag=php&page=2&per_page=20&sort=created_at&order=desc

2. 响应格式

  • 使用JSON作为响应体格式(Content-Type: application/json)
  • 响应应该包含资源的完整表示,包括ID、创建时间、更新时间等元数据
  • 时间使用ISO 8601格式(如2016-05-18T20:00:00+08:00)
  • 列表响应应该包含数据数组和分页信息
# 单个资源响应
GET /users/123
200 OK
Content-Type: application/json

{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 26,
  "created_at": "2016-01-01T10:00:00+08:00",
  "updated_at": "2016-05-18T20:00:00+08:00"
}

# 资源列表响应
GET /articles?page=2&per_page=20
200 OK
Content-Type: application/json

{
  "data": [
    {
      "id": 1,
      "title": "文章标题",
      "content": "文章内容...",
      "author": {
        "id": 123,
        "name": "张三"
      },
      "created_at": "2016-05-18T20:00:00+08:00"
    }
    // ... 更多文章
  ],
  "meta": {
    "current_page": 2,
    "per_page": 20,
    "total": 156,
    "total_pages": 8,
    "from": 21,
    "to": 40
  },
  "links": {
    "first": "/articles?page=1&per_page=20",
    "last": "/articles?page=8&per_page=20",
    "prev": "/articles?page=1&per_page=20",
    "next": "/articles?page=3&per_page=20"
  }
}

3. 错误响应格式

错误响应应该包含错误码、错误消息、错误详情等信息,方便客户端排查问题。

# 错误响应
422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确"
      },
      {
        "field": "password",
        "message": "密码长度不能少于6位"
      }
    ]
  }
}

# 简单错误响应
404 Not Found
Content-Type: application/json

{
  "error": {
    "code": "NOT_FOUND",
    "message": "请求的资源不存在"
  }
}

分页、过滤、排序、搜索

1. 分页

列表接口应该支持分页,避免一次返回过多数据。常用的分页方式有两种:页码分页和游标分页。

# 页码分页
GET /articles?page=1&per_page=20

# 响应中包含分页信息
{
  "data": [...],
  "meta": {
    "current_page": 1,
    "per_page": 20,
    "total": 156,
    "total_pages": 8
  }
}

# 游标分页(适合大数据量,性能更好)
GET /articles?limit=20&cursor=eyJpZCI6MTIzfQ==

# 响应中包含下一页游标
{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6NDU2fQ==",
    "has_more": true
  }
}

2. 过滤

通过查询参数对资源进行过滤,如按分类、标签、状态、时间范围等过滤。

# 按分类过滤
GET /articles?category=tech

# 按标签过滤
GET /articles?tag=php

# 按状态过滤
GET /articles?status=published

# 按作者过滤
GET /articles?author_id=123

# 按时间范围过滤
GET /articles?created_from=2016-01-01&created_to=2016-05-31

# 多条件组合
GET /articles?category=tech&status=published&author_id=123

3. 排序

通过查询参数指定排序字段和排序方向。

# 按创建时间倒序(最新的在前)
GET /articles?sort=created_at&order=desc

# 按浏览量倒序(最热的在前)
GET /articles?sort=views&order=desc

# 按标题正序
GET /articles?sort=title&order=asc

# 多字段排序
GET /articles?sort=is_top,created_at&order=desc,desc

4. 搜索

全文搜索通常使用q参数。

# 搜索关键词
GET /articles?q=PHP

# 搜索+过滤+分页+排序组合
GET /articles?q=PHP&category=tech&status=published&page=1&per_page=20&sort=created_at&order=desc

版本控制

API会不断演进,需要版本控制来保证旧客户端的兼容性。

1. 版本控制方式

  • URI路径版本:在URI中包含版本号,如/v1/users、/v2/users,简单直观,最常用
  • 查询参数版本:在查询参数中指定版本,如/users?version=1,不太推荐
  • 请求头版本:在Accept头中指定版本,如Accept: application/vnd.myapp.v1+json,比较RESTful但不够直观
  • 主机名版本:使用不同的子域名,如v1.api.example.com/users,适合大公司

2. 推荐方式

推荐使用URI路径版本,简单直观,易于理解和调试。

# URI路径版本
GET /v1/users
GET /v2/users

# 同时维护多个版本
/v1/articles    # 旧版本,保持兼容
/v2/articles    # 新版本,新功能

3. 版本演进原则

  • 向后兼容的修改不需要升版本:如添加新字段、添加新接口、添加可选参数
  • 破坏性修改需要升版本:如删除字段、修改字段类型、修改接口语义、删除接口
  • 旧版本要有弃用计划:提前通知用户,给出迁移时间,最终下线旧版本
  • 文档中明确版本差异:说明每个版本的变更,帮助用户迁移

认证和授权

1. 认证方式

  • API Key:在请求头或查询参数中携带API Key,简单但安全性较低,适合公开API
  • Basic Auth:用户名密码Base64编码,简单但安全性低,不推荐生产环境使用
  • Bearer Token(JWT):使用JSON Web Token,无状态,适合前后端分离和微服务
  • OAuth 2.0:第三方授权框架,适合需要第三方登录和授权的场景
  • Session/Cookie:传统的会话认证,有状态,适合传统Web应用

2. 推荐方式

对于RESTful API,推荐使用Bearer Token(JWT)方式,无状态,易于扩展和负载均衡。

# 请求头中携带Token
GET /users/123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Token过期返回401
401 Unauthorized
{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "登录已过期,请重新登录"
  }
}

3. 授权

认证解决"你是谁"的问题,授权解决"你能做什么"的问题。常用的授权模型有:

  • ACL(访问控制列表):每个资源有访问控制列表,简单但灵活性差
  • RBAC(基于角色的访问控制):用户分配角色,角色分配权限,最常用
  • ABAC(基于属性的访问控制):根据用户、资源、环境的属性动态判断权限,灵活但复杂

RESTful API中,授权失败应该返回403 Forbidden。

# 无权限访问
403 Forbidden
{
  "error": {
    "code": "FORBIDDEN",
    "message": "您没有权限执行此操作"
  }
}

安全最佳实践

  1. 使用HTTPS:所有API都应该使用HTTPS加密传输,防止数据被窃听和篡改
  2. 认证和授权:对需要保护的接口进行认证和授权,防止未授权访问
  3. 输入验证:对所有输入参数进行验证,防止SQL注入、XSS、CSRF等攻击
  4. 限流:对API进行限流,防止滥用和DDoS攻击,超过限制返回429
  5. 敏感信息保护:不要在响应中返回密码、密钥等敏感信息,日志中也要脱敏
  6. CORS配置:合理配置跨域资源共享,只允许信任的域名访问
  7. 安全头:设置合适的安全响应头,如X-Content-Type-Options、X-Frame-Options、X-XSS-Protection等
  8. 版本弃用:及时弃用和下线有安全问题的旧版本API
  9. 错误信息:生产环境不要返回详细的错误堆栈信息,避免泄露系统内部信息
  10. 审计日志:对重要操作记录审计日志,便于安全审计和问题排查

文档

API文档是API的重要组成部分,好的文档能大大降低使用者的学习成本。

1. 文档内容

  • API概述:介绍API的用途、认证方式、基础URL、版本信息
  • 认证说明:如何获取和使用Token,Token过期处理
  • 接口列表:每个接口的URI、HTTP方法、描述、请求参数、响应格式、错误码
  • 请求示例:每个接口的请求示例,包括请求头、请求体
  • 响应示例:每个接口的响应示例,包括成功和错误响应
  • 错误码说明:所有可能的错误码和含义
  • 变更日志:API的版本变更历史

2. 文档工具

  • Swagger/OpenAPI:最流行的API文档规范,支持自动生成交互式文档
  • API Blueprint:另一种API文档规范,使用Markdown格式
  • RAML:RESTful API建模语言
  • Postman:API测试工具,也可以生成文档
  • ReadMe、GitBook:文档托管平台

推荐使用Swagger/OpenAPI规范,可以自动生成文档和客户端代码,保持文档和代码同步。

总结

RESTful API设计是一门艺术,也是一门科学。好的API设计应该遵循REST原则,使用标准的HTTP方法和状态码,设计直观一致的URI,提供清晰的请求和响应格式,支持分页、过滤、排序、搜索,做好版本控制、认证授权、安全防护和文档。

API一旦发布,就会被使用者依赖,所以设计时要慎重,尽量保持稳定和向后兼容。同时,API也需要不断演进,通过版本控制平滑升级,在创新和兼容之间找到平衡。

希望这篇文章能帮助大家设计出优雅、易用、可维护的RESTful API,让前后端协作更加顺畅,让系统更加健壮和可扩展。

记住,好的API是给人用的,不仅仅是给机器用的。设计API时,多站在使用者的角度思考,让API简单、直观、一致、可预测,这就是最好的API设计。