在现代Web开发中,API(Application Programming Interface,应用程序接口)是前后端分离、微服务架构、第三方集成的基础。而REST(Representational State Transfer,表述性状态转移)是目前最流行的API设计风格,几乎所有的Web API都自称是RESTful的。

但是,真正设计良好的RESTful API并不多。很多API虽然叫RESTful,但是设计混乱、URL不规范、HTTP方法用错、状态码乱用、错误处理不一致,导致使用起来非常痛苦。一个好的API应该是:直观、易用、一致、可预测、易维护、易扩展。

这些年,我设计和使用过不少API,有自己设计的,也有第三方的。深刻体会到好的API设计能大大提升开发效率,差的API设计会让人抓狂。今天就来系统地分享一下RESTful API设计的最佳实践,从基本概念到具体细节,帮助你设计出优雅、易用、可维护的API。

一、REST的基本概念

1. 什么是REST

REST是由Roy Fielding在2000年的博士论文中提出的一种软件架构风格。REST不是标准,也不是协议,而是一组设计原则和约束条件。

REST的核心思想是:将所有事物都抽象为"资源"(Resource),通过URL标识资源,通过HTTP方法操作资源,通过表述(Representation)传递资源状态。

2. REST的六大约束

Roy Fielding提出了REST的六大约束,满足这些约束的API才能称为RESTful:

  1. 客户端-服务器分离(Client-Server):客户端和服务器分离,客户端负责用户界面,服务器负责数据存储和业务逻辑。两者通过API交互,可以独立演进。
  2. 无状态(Stateless):服务器不保存客户端的状态,每个请求都必须包含所有必要的信息。服务器可以随时处理任何请求,不需要会话信息。这提高了可扩展性和可靠性。
  3. 可缓存(Cacheable):响应必须明确标记是否可缓存,客户端可以缓存响应以减少重复请求。这提高了性能和可扩展性。
  4. 统一接口(Uniform Interface):API的接口应该统一、一致,包括资源标识、通过表述操作资源、自描述的消息、超媒体作为应用状态的引擎(HATEOAS)。
  5. 分层系统(Layered System):系统可以分层,客户端无法区分是直接连接到终端服务器还是中间层。中间层可以提供负载均衡、缓存、安全等功能。
  6. 按需代码(Code on Demand,可选):服务器可以向客户端传输代码(如JavaScript),客户端可以执行。这是可选约束,大多数RESTful API不使用。

3. REST的核心概念

  • 资源(Resource):REST将所有事物都抽象为资源。资源可以是一个用户、一篇文章、一个订单、一个集合等。资源是名词,不是动词。
  • 资源标识符(URI/URL):每个资源都有唯一的标识符(URL)。如/users/123表示id为123的用户。
  • 表述(Representation):资源的表述是资源在某个时刻的状态的表示,通常是JSON或XML格式。客户端和服务器通过表述传递资源状态。
  • 状态转移(State Transfer):通过HTTP方法(GET、POST、PUT、DELETE等)操作资源,实现资源状态的转移。如POST创建资源,PUT更新资源,DELETE删除资源。
  • 统一接口:使用标准的HTTP方法和状态码,接口一致、可预测。

二、URL设计

URL是API的门面,好的URL设计能让API直观、易用、易记。

1. 使用名词,不用动词

资源是名词,URL应该用名词表示资源,而不是用动词表示操作。操作由HTTP方法表示。

正确:

GET    /users          # 获取用户列表
GET    /users/123      # 获取id为123的用户
POST   /users          # 创建用户
PUT    /users/123      # 更新id为123的用户
DELETE /users/123      # 删除id为123的用户

错误:

GET    /getUsers       # 错误:用了动词get
GET    /users/list     # 错误:list是多余的
POST   /createUser     # 错误:用了动词create
POST   /users/123/delete  # 错误:delete是动词,应该用DELETE方法

2. 使用复数名词

表示集合的URL应该用复数名词,保持一致性。

/users          # 用户集合
/users/123      # 单个用户
/articles       # 文章集合
/articles/456   # 单篇文章
/orders         # 订单集合
/orders/789     # 单个订单

虽然有些情况用单数也说得通(如/user/123),但是统一用复数更一致、更易记。

3. 层级关系用斜杠

资源之间的层级关系用斜杠(/)表示。

/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层就够了。太深的层级会让URL变长、变复杂。如果需要跨资源查询,可以用查询参数。

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

非资源层级的操作(过滤、排序、分页、搜索等)用查询参数(?key=value)表示。

/articles?category=tech           # 筛选分类为tech的文章
/articles?status=published        # 筛选状态为published的文章
/articles?sort=created_at&order=desc  # 按创建时间倒序排序
/articles?page=2&per_page=20      # 分页,第2页,每页20条
/articles?keyword=php              # 搜索关键词为php的文章
/articles?author_id=123&category=tech  # 多条件组合

查询参数的命名要一致、直观。常用的:

  • 过滤:?field=value(如?status=published
  • 排序:?sort=field&order=asc|desc
  • 分页:?page=1&per_page=20?offset=0&limit=20
  • 搜索:?q=keyword?keyword=keyword
  • 字段筛选:?fields=id,title,created_at

5. URL全部小写,用连字符分隔

URL路径全部小写,多个单词用连字符(-)分隔,不要用下划线(_)或驼峰命名。

/article-categories    # 正确:小写+连字符
/article_categories    # 不推荐:下划线
/articleCategories     # 不推荐:驼峰

查询参数可以用下划线或驼峰,但是要保持一致。建议用下划线(如createdatperpage),因为数据库字段通常用下划线。

6. 版本控制

API需要版本控制,以便在不破坏旧客户端的情况下演进API。

常见的版本控制方式:

  • URL路径版本/v1/users/v2/users(最常用、最直观)
  • 查询参数版本/users?version=1
  • 请求头版本Accept: application/vnd.myapp.v1+json

推荐使用URL路径版本,因为最直观、最易调试。

/v1/users
/v1/articles
/v2/users  # 新版本,不兼容的改动

版本号用整数(v1、v2),不要用小数(v1.1)。不兼容的改动才升级大版本,兼容的改动(如增加字段、增加接口)不需要升级版本。

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

不要在URL中加.json.xml等扩展名,格式应该通过Accept请求头或查询参数协商。

/users          # 正确
/users.json     # 不推荐

如果需要支持多种格式,可以用查询参数?format=json或Accept头Accept: application/json

三、HTTP方法

HTTP方法表示对资源的操作,RESTful API应该正确使用HTTP方法的语义。

1. 常用HTTP方法

方法语义幂等安全示例
GET获取资源GET /users/123
POST创建资源POST /users
PUT全量更新资源(替换)PUT /users/123
PATCH部分更新资源PATCH /users/123
DELETE删除资源DELETE /users/123
HEAD获取资源的元数据(头信息)HEAD /users/123
OPTIONS获取资源支持的方法OPTIONS /users

2. 幂等性和安全性

  • 幂等(Idempotent):多次执行同一个请求,结果和执行一次相同。GET、PUT、DELETE是幂等的,POST、PATCH不是。
  • 安全(Safe):请求不会改变服务器状态。GET、HEAD、OPTIONS是安全的,POST、PUT、PATCH、DELETE不是。

幂等性很重要,因为网络可能超时,客户端可能重试。幂等的方法可以安全重试,非幂等的方法重试可能导致重复创建或错误更新。

3. 各方法详解

GET

  • 用于获取资源(单个或列表)
  • 不应该改变服务器状态
  • 可以被缓存
  • 应该是幂等的
  • 示例:

`` GET /users # 获取用户列表 GET /users/123 # 获取id为123的用户 GET /users/123/articles # 获取id为123的用户的文章 ``

POST

  • 用于创建新资源
  • 不是幂等的(多次POST会创建多个资源)
  • 新资源的URL通常在响应的Location头中返回
  • 示例:

`` POST /users # 创建新用户,请求体包含用户信息 POST /users/123/articles # 为id为123的用户创建新文章 ``

PUT

  • 用于全量更新资源(用请求体替换整个资源)
  • 是幂等的(多次PUT结果相同)
  • 如果资源不存在,有些API会创建资源(但推荐用POST创建)
  • 示例:

`` PUT /users/123 # 全量更新id为123的用户,请求体包含完整的用户信息 ``

PATCH

  • 用于部分更新资源(只更新请求体中指定的字段)
  • 不是幂等的(虽然很多实现是幂等的,但标准不保证)
  • 2010年成为RFC 5789标准
  • 示例:

`` PATCH /users/123 # 部分更新id为123的用户,请求体只包含要更新的字段 ``

PUT vs PATCH:

  • PUT:全量更新,必须提供完整的资源数据,未提供的字段会被清空或设为默认值
  • PATCH:部分更新,只提供要更新的字段,未提供的字段保持不变
  • 如果只更新少数几个字段,用PATCH更高效;如果替换整个资源,用PUT

DELETE

  • 用于删除资源
  • 是幂等的(多次删除同一个资源结果相同)
  • 删除后通常返回204 No Content或200 OK
  • 示例:

`` DELETE /users/123 # 删除id为123的用户 ``

4. 动作(Action)的处理

有时候需要对资源执行一些不是CRUD的动作(如"发布文章"、"点赞"、"转发")。这时候有几种处理方式:

方式1:将动作抽象为子资源

POST /articles/456/publish    # 发布文章(创建一个"发布"动作资源)
POST /articles/456/like       # 点赞(创建一个"点赞"资源)
POST /articles/456/share      # 转发(创建一个"转发"资源)

方式2:用PATCH更新状态字段

PATCH /articles/456           # 更新文章状态为published
{ "status": "published" }

方式3:用查询参数(不推荐,因为GET不应该改变状态)

POST /articles/456?action=publish  # 不推荐

推荐方式1(动作抽象为子资源)或方式2(PATCH更新状态),保持REST风格。

四、HTTP状态码

HTTP状态码表示请求的结果,RESTful API应该正确使用状态码,不要所有响应都返回200。

1. 常用状态码

2xx 成功

  • 200 OK:请求成功,GET/PUT/PATCH成功时返回
  • 201 Created:资源创建成功,POST成功时返回,通常带Location头
  • 202 Accepted:请求已接受,正在处理(异步任务)
  • 204 No Content:请求成功,但是没有响应体,DELETE成功时常用

3xx 重定向

  • 301 Moved Permanently:资源永久移动
  • 302 Found:资源临时移动
  • 304 Not Modified:资源未修改,可以使用缓存(配合ETag/Last-Modified)

4xx 客户端错误

  • 400 Bad Request:请求参数错误、格式错误
  • 401 Unauthorized:未认证,需要登录
  • 403 Forbidden:已认证,但是没有权限
  • 404 Not Found:资源不存在
  • 405 Method Not Allowed:HTTP方法不允许(如对只读资源用POST)
  • 409 Conflict:资源冲突(如重复创建、版本冲突)
  • 410 Gone:资源已永久删除
  • 415 Unsupported Media Type:不支持的媒体类型
  • 422 Unprocessable Entity:请求格式正确,但是语义错误(如验证失败)
  • 429 Too Many Requests:请求过于频繁(限流)

5xx 服务器错误

  • 500 Internal Server Error:服务器内部错误
  • 501 Not Implemented:服务器不支持该功能
  • 502 Bad Gateway:网关错误(反向代理时后端不可用)
  • 503 Service Unavailable:服务不可用(维护、过载)
  • 504 Gateway Timeout:网关超时

2. 状态码使用建议

  • GET:成功返回200,资源不存在返回404
  • POST:创建成功返回201(带Location头),参数错误返回400,验证失败返回422,重复创建返回409
  • PUT/PATCH:更新成功返回200,资源不存在返回404,参数错误返回400,验证失败返回422
  • DELETE:删除成功返回204(无响应体)或200,资源不存在返回404
  • 认证/权限:未登录返回401,无权限返回403
  • 限流:返回429,带Retry-After头
  • 服务器错误:返回500,不要把错误信息暴露给客户端

3. 不要所有响应都返回200

很多API设计错误地把所有响应都返回200,然后在响应体中用code字段表示错误。这不符合HTTP语义,也不利于缓存、代理、客户端处理。

错误:

// HTTP 200
{ "code": 404, "message": "用户不存在" }

正确:

// HTTP 404
{ "error": "not_found", "message": "用户不存在" }

五、请求和响应格式

1. 使用JSON

现代RESTful API通常使用JSON作为请求和响应的格式。JSON轻量、易读、广泛支持。

  • 请求体:Content-Type: application/json
  • 响应体:Content-Type: application/json; charset=utf-8

如果需要支持XML,可以用查询参数?format=xml或Accept头协商,但是JSON是默认。

2. 响应格式一致

所有响应的格式应该一致,包括成功响应和错误响应。

成功响应(单个资源):

{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com",
  "created_at": "2016-08-16T10:00:00Z",
  "updated_at": "2016-08-16T10:00:00Z"
}

成功响应(资源列表):

{
  "data": [
    { "id": 123, "name": "张三" },
    { "id": 456, "name": "李四" }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 100,
    "total_pages": 5
  }
}

错误响应:

{
  "error": "validation_failed",
  "message": "请求参数验证失败",
  "errors": {
    "email": ["邮箱格式不正确"],
    "password": ["密码至少6位"]
  }
}

3. 字段命名一致

字段命名要一致,建议:

  • 用下划线命名(snakecase):createdatuseridarticletitle
  • 或者用驼峰命名(camelCase):createdAtuserIdarticleTitle
  • 不要混用

推荐用下划线,因为和数据库字段一致,Python/Ruby/PHP等后端语言也常用下划线。

4. 时间格式

时间用ISO 8601格式,带时区:

"created_at": "2016-08-16T10:00:00Z"       # UTC时间
"created_at": "2016-08-16T18:00:00+08:00"  # 带时区

不要用时间戳(如1471312800),因为不直观、有时区歧义。如果需要时间戳,可以额外提供一个字段。

5. 分页

列表接口应该支持分页,避免返回太多数据。

分页参数:

  • page:页码,从1开始
  • per_page:每页数量,默认20,最大100

响应中包含分页信息:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 100,
    "total_pages": 5
  }
}

也可以用游标分页(cursor-based pagination),适合大数据量和实时数据:

/articles?cursor=abc123&limit=20

6. 排序

列表接口支持排序:

  • sort:排序字段,如sort=created_at
  • order:排序方向,ascdesc,默认desc

多字段排序:

/articles?sort=category,created_at&order=asc,desc

7. 过滤

列表接口支持过滤,用查询参数:

/articles?category=tech&status=published&author_id=123

范围过滤:

/articles?created_at_from=2016-01-01&created_at_to=2016-12-31
/articles?views_min=1000&views_max=10000

8. 字段筛选

客户端可以指定只返回需要的字段,减少数据传输:

/users?fields=id,name,email
/articles?fields=id,title,excerpt,created_at

9. 搜索

全文搜索用qkeyword参数:

/articles?q=php
/articles?keyword=mysql

六、认证和安全

1. 认证方式

  • API Key:在请求头或查询参数中传递API Key,适合服务端到服务端的认证。

`` Authorization: Bearer YOURAPIKEY ?apikey=YOURAPI_KEY ``

  • Token(JWT):用户登录后返回Token,后续请求在Authorization头中传递。适合Web和移动端。

`` Authorization: Bearer YOURJWTTOKEN ``

  • OAuth 2.0:第三方授权认证,适合需要第三方登录或授权的场景。
  • Session/Cookie:传统Web应用的会话认证,不适合纯API(无状态约束)。

推荐用Token(JWT)或OAuth 2.0,保持API无状态。

2. HTTPS

所有API都应该使用HTTPS,特别是需要认证的API。HTTPS加密传输,防止窃听和篡改。

  • 所有请求强制HTTPS,HTTP重定向到HTTPS
  • 启用HSTS(HTTP Strict Transport Security)
  • 使用TLS 1.2+,禁用旧的不安全协议
  • 证书要有效、可信

3. 权限控制

  • 认证(Authentication):确认你是谁(登录)
  • 授权(Authorization):确认你能做什么(权限)

每个API都应该检查权限:

  • 未登录返回401
  • 已登录但无权限返回403
  • 不要在前端做权限控制,后端必须校验

4. 输入验证

所有输入都必须验证,防止SQL注入、XSS、CSRF等攻击。

  • 验证参数类型、格式、范围
  • 验证必填参数
  • 过滤特殊字符
  • 使用参数化查询(防止SQL注入)
  • 输出转义(防止XSS)
  • CSRF Token(如果用Cookie认证)

5. 限流(Rate Limiting)

API应该限流,防止滥用和DDoS攻击。

  • 按用户/IP限流,如每分钟60次
  • 超过限制返回429 Too Many Requests
  • 在响应头中返回限流信息:

`` X-RateLimit-Limit: 60 X-RateLimit-Remaining: 59 X-RateLimit-Reset: 1471312800 Retry-After: 60 ``

6. 敏感信息保护

  • 不要在响应中返回密码、密码哈希、Token等敏感信息
  • 不要在日志中记录密码、Token等敏感信息
  • 错误信息不要暴露服务器内部细节(如文件路径、SQL语句、堆栈跟踪)
  • 用户ID不要用自增整数(可被枚举),可以用UUID或混淆ID

七、API文档

好的API需要好的文档。文档是API的使用说明,应该清晰、完整、易搜索。

1. 文档工具

  • Swagger / OpenAPI:最流行的API文档工具,用YAML/JSON定义API,自动生成交互式文档。
  • API Blueprint:用Markdown写API文档。
  • RAML:RESTful API Modeling Language。
  • Postman:可以生成API文档和测试。

推荐用Swagger / OpenAPI,因为生态最完善,支持代码生成、测试、Mock等。

2. 文档内容

API文档应该包含:

  • 概述:API的用途、基础URL、版本
  • 认证:如何获取和使用Token
  • 通用说明:请求格式、响应格式、错误码、分页、排序、限流
  • 每个接口的详细说明:

- 接口描述 - HTTP方法和URL - 请求参数(路径参数、查询参数、请求体) - 响应示例(成功和失败) - 状态码 - 权限要求

  • 示例代码:常用语言的调用示例
  • 变更日志:API的版本变更记录

3. 文档要和代码同步

文档最常见的问题是和代码不同步。最好的方式是:

  • 用Swagger/OpenAPI定义API,从定义生成代码和文档
  • 或者从代码注释生成文档(如Swagger注解)
  • 持续集成中检查文档是否更新
  • 版本变更时更新文档和变更日志

八、版本控制

1. 什么时候需要升级版本

  • 不兼容的改动(需要升级大版本):

- 删除接口 - 删除或重命名字段 - 改变字段类型 - 改变接口语义 - 改变默认行为

  • 兼容的改动(不需要升级版本):

- 增加新接口 - 增加可选字段 - 增加新的枚举值 - 优化性能 - 修复bug(不改变预期行为)

2. 版本控制策略

  • URL路径版本:/v1/users/v2/users(推荐)
  • 同时维护多个版本,给旧客户端迁移时间
  • 旧版本标记为deprecated,设置下线时间
  • 版本变更时通知用户,提供迁移指南

九、性能优化

1. 缓存

  • GET请求应该支持缓存,用ETag和Last-Modified头
  • 客户端可以发If-None-Match和If-Modified-Since,服务器返回304 Not Modified
  • 不常变化的数据可以用CDN缓存
  • 服务器端用Redis等缓存热点数据

2. 分页

  • 列表接口必须分页,避免返回大量数据
  • 限制每页最大数量(如100)
  • 深分页优化(如用游标分页或禁止跳转到太深的页)

3. 压缩

  • 启用Gzip或Brotli压缩响应体
  • 客户端用Accept-Encoding头声明支持的压缩方式

4. 异步处理

  • 耗时的操作(如发送邮件、生成报告、批量处理)应该异步处理
  • 接口立即返回202 Accepted,后台处理,客户端轮询或回调获取结果

5. 数据库优化

  • 合理使用索引
  • 避免N+1查询,用JOIN或预加载
  • 只查询需要的字段
  • 读写分离(读多写少的场景)

十、常见错误

1. URL用动词 错误:/getUsers/createArticle/deleteUser/123 正确:用名词+HTTP方法:GET /usersPOST /articlesDELETE /users/123

2. 所有响应都返回200 错误:HTTP 200 + { "code": 404 } 正确:用正确的HTTP状态码,404就返回HTTP 404

3. GET请求改变状态 错误:GET /users/123/deleteGET /articles/456/publish 正确:改变状态用POST/PUT/PATCH/DELETE,GET只用于获取

4. 不做输入验证 错误:直接使用客户端传入的参数,不验证 正确:所有输入都验证类型、格式、范围、权限

5. 错误信息不清晰 错误:{ "message": "error" } 正确:{ "error": "validation_failed", "message": "邮箱格式不正确", "errors": { "email": ["邮箱格式不正确"] } }

6. 没有版本控制 错误:直接修改API,破坏旧客户端 正确:用版本控制,不兼容的改动升级版本

7. 没有文档或文档过时 错误:没有文档,或者文档和代码不一致 正确:用Swagger等工具维护文档,和代码同步更新

8. 分页参数不统一 错误:有的接口用page,有的用offset,有的用p 正确:统一分页参数,如pageper_page

9. 返回敏感信息 错误:返回密码哈希、Token、内部错误信息 正确:只返回必要的字段,错误信息不暴露内部细节

10. 不限流 错误:API没有限流,容易被滥用或攻击 正确:实现限流,超过限制返回429

总结

RESTful API设计是一门艺术,也是一门科学。好的API设计能让接口直观、易用、一致、可预测、易维护、易扩展。

REST的核心概念:

  • 资源(名词):所有事物抽象为资源
  • URL:标识资源,用名词复数、层级关系、查询参数
  • HTTP方法:操作资源,GET获取、POST创建、PUT全量更新、PATCH部分更新、DELETE删除
  • 状态码:表示请求结果,正确使用2xx/3xx/4xx/5xx
  • 表述:JSON格式,字段命名一致,时间用ISO 8601

API设计最佳实践:

  1. URL用名词不用动词,用复数,层级用斜杠,查询参数用于过滤排序分页
  2. 正确使用HTTP方法和状态码,不要所有响应都返回200
  3. 请求和响应用JSON,格式一致,字段命名一致
  4. 支持分页、排序、过滤、字段筛选、搜索
  5. 用HTTPS、Token认证、权限控制、输入验证、限流
  6. 用Swagger/OpenAPI维护文档,文档和代码同步
  7. 版本控制,不兼容的改动升级版本
  8. 性能优化:缓存、分页、压缩、异步处理、数据库优化

常见错误:URL用动词、所有响应返回200、GET改变状态、不验证输入、错误信息不清晰、没有版本控制、文档过时、分页不统一、返回敏感信息、不限流。

最后,API设计没有绝对的标准答案,关键是一致性和可用性。在遵循REST原则的基础上,根据实际场景做出合理的设计。好的API应该让使用者"不需要看文档就能猜到怎么用",这才是API设计的最高境界。

愿我们都能设计出优雅、易用、可维护的API,让前后端协作更愉快,让第三方集成交付更顺畅。