在现代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:
- 客户端-服务器分离(Client-Server):客户端和服务器分离,客户端负责用户界面,服务器负责数据存储和业务逻辑。两者通过API交互,可以独立演进。
- 无状态(Stateless):服务器不保存客户端的状态,每个请求都必须包含所有必要的信息。服务器可以随时处理任何请求,不需要会话信息。这提高了可扩展性和可靠性。
- 可缓存(Cacheable):响应必须明确标记是否可缓存,客户端可以缓存响应以减少重复请求。这提高了性能和可扩展性。
- 统一接口(Uniform Interface):API的接口应该统一、一致,包括资源标识、通过表述操作资源、自描述的消息、超媒体作为应用状态的引擎(HATEOAS)。
- 分层系统(Layered System):系统可以分层,客户端无法区分是直接连接到终端服务器还是中间层。中间层可以提供负载均衡、缓存、安全等功能。
- 按需代码(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 # 不推荐:驼峰查询参数可以用下划线或驼峰,但是要保持一致。建议用下划线(如createdat、perpage),因为数据库字段通常用下划线。
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):
createdat、userid、articletitle - 或者用驼峰命名(camelCase):
createdAt、userId、articleTitle - 不要混用
推荐用下划线,因为和数据库字段一致,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=206. 排序
列表接口支持排序:
sort:排序字段,如sort=created_atorder:排序方向,asc或desc,默认desc
多字段排序:
/articles?sort=category,created_at&order=asc,desc7. 过滤
列表接口支持过滤,用查询参数:
/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=100008. 字段筛选
客户端可以指定只返回需要的字段,减少数据传输:
/users?fields=id,name,email
/articles?fields=id,title,excerpt,created_at9. 搜索
全文搜索用q或keyword参数:
/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 /users、POST /articles、DELETE /users/123
2. 所有响应都返回200 错误:HTTP 200 + { "code": 404 } 正确:用正确的HTTP状态码,404就返回HTTP 404
3. GET请求改变状态 错误:GET /users/123/delete、GET /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 正确:统一分页参数,如page和per_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设计最佳实践:
- URL用名词不用动词,用复数,层级用斜杠,查询参数用于过滤排序分页
- 正确使用HTTP方法和状态码,不要所有响应都返回200
- 请求和响应用JSON,格式一致,字段命名一致
- 支持分页、排序、过滤、字段筛选、搜索
- 用HTTPS、Token认证、权限控制、输入验证、限流
- 用Swagger/OpenAPI维护文档,文档和代码同步
- 版本控制,不兼容的改动升级版本
- 性能优化:缓存、分页、压缩、异步处理、数据库优化
常见错误:URL用动词、所有响应返回200、GET改变状态、不验证输入、错误信息不清晰、没有版本控制、文档过时、分页不统一、返回敏感信息、不限流。
最后,API设计没有绝对的标准答案,关键是一致性和可用性。在遵循REST原则的基础上,根据实际场景做出合理的设计。好的API应该让使用者"不需要看文档就能猜到怎么用",这才是API设计的最高境界。
愿我们都能设计出优雅、易用、可维护的API,让前后端协作更愉快,让第三方集成交付更顺畅。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录