API 接口设计规范——RESTful 最佳实践
作者: CaoZH
日期: 2025-06-15
本文为原创教程
当你和团队一起开发时,API 接口设计是否规范直接影响前后端的协作效率。一份好的 API 规范,能让前端开发者不用问”这个接口返回什么字段”就能直接开发。
本文总结了一套经过多个项目验证的 RESTful API 设计规范,涵盖命名、状态码、分页、错误处理等关键方面。
一、基础规范
URL 命名
1 2 3 4 5 6 7 8 9 10 11 12 13
| ## 核心原则 - 使用名词复数:/users 而非 /user - 使用小写 + 连字符:/order-items 而非 /orderItems - 使用版本号:/api/v1/users - 层级关系用路径表示:/users/123/orders
## 示例 GET /api/v1/users # 用户列表 GET /api/v1/users/123 # 单个用户 POST /api/v1/users # 创建用户 PUT /api/v1/users/123 # 更新用户 DELETE /api/v1/users/123 # 删除用户 GET /api/v1/users/123/orders # 用户的订单列表
|
HTTP 方法语义
1 2 3 4 5 6 7
| | 方法 | 语义 | 是否幂等 | 请求体 | 响应体 | |------|------|---------|--------|--------| | GET | 查询 | ✅ | 无 | 数据 | | POST | 创建 | ❌ | 有 | 创建成功的资源 | | PUT | 全量更新 | ✅ | 有 | 更新后的资源 | | PATCH | 部分更新 | ✅ | 有 | 更新后的资源 | | DELETE | 删除 | ✅ | 无 | 空/删除状态 |
|
二、统一响应格式
成功响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "code": 200, "message": "操作成功", "data": { "id": 1, "username": "admin", "email": "admin@example.com", "createdAt": "2025-06-15T10:00:00Z" } }
{ "code": 200, "message": "查询成功", "data": { "list": [ { "id": 1, "username": "admin" }, { "id": 2, "username": "test" } ], "total": 100, "page": 1, "pageSize": 20 } }
|
错误响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| { "code": 400, "message": "参数校验失败", "errors": [ { "field": "email", "message": "邮箱格式不正确" }, { "field": "age", "message": "年龄必须大于0" } ] }
{ "code": 401, "message": "未登录或 Token 已过期" }
{ "code": 403, "message": "权限不足" }
{ "code": 404, "message": "用户不存在" }
{ "code": 500, "message": "服务器内部错误,请稍后重试" }
|
三、状态码使用指南
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ## 2xx — 成功 | 状态码 | 含义 | 使用场景 | |--------|------|---------| | 200 | OK | GET 查询成功,PUT/PATCH 更新成功 | | 201 | Created | POST 创建成功 | | 204 | No Content | DELETE 删除成功 |
## 3xx — 重定向 | 状态码 | 含义 | 使用场景 | |--------|------|---------| | 301 | Moved Permanently | 资源永久迁移 | | 302 | Found | 临时重定向 | | 304 | Not Modified | 缓存有效 |
## 4xx — 客户端错误 | 状态码 | 含义 | 使用场景 | |--------|------|---------| | 400 | Bad Request | 参数错误、格式错误 | | 401 | Unauthorized | 未登录、Token 失效 | | 403 | Forbidden | 已登录但无权限 | | 404 | Not Found | 资源不存在 | | 405 | Method Not Allowed | 接口不支持该 HTTP 方法 | | 409 | Conflict | 资源冲突(用户名重复等) | | 422 | Unprocessable Entity | 业务校验不通过 | | 429 | Too Many Requests | 请求频率过高 |
## 5xx — 服务端错误 | 状态码 | 含义 | 使用场景 | |--------|------|---------| | 500 | Internal Server Error | 服务器内部错误 | | 502 | Bad Gateway | 上游服务不可用 | | 503 | Service Unavailable | 服务暂时不可用 | | 504 | Gateway Timeout | 上游服务超时 |
|
四、分页规范
请求
1
| GET /api/v1/users?page=1&pageSize=20&sort=-createdAt&status=active
|
| 参数 |
类型 |
默认值 |
说明 |
| page |
int |
1 |
页码 |
| pageSize |
int |
20 |
每页条数(最大 100) |
| sort |
string |
-createdAt |
排序字段,- 前缀表示降序 |
| 其他 |
- |
- |
查询过滤条件 |
响应
1 2 3 4 5 6 7 8 9 10 11
| { "code": 200, "message": "查询成功", "data": { "list": [...], "total": 1000, "page": 1, "pageSize": 20, "totalPages": 50 } }
|
五、认证与鉴权
1 2
| # 请求头中携带 Token Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Component public class JwtAuthFilter extends OncePerRequestFilter {
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
String token = extractToken(request); if (token != null) { try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("username", claims.get("sub")); } catch (Exception e) { response.setStatus(401); return; } } chain.doFilter(request, response); }
private String extractToken(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { return header.substring(7); } return null; } }
|
六、接口文档
Swagger / Knife4j 配置
1 2 3 4 5
| <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>4.3.0</version> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Configuration @EnableSwagger2 public class SwaggerConfig {
@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder() .title("用户管理系统 API") .version("v1") .build()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.any()) .build(); } }
|
七、最佳实践清单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ## 设计阶段 □ 使用名词复数命名资源 □ 版本号放在 URL 中 □ 设计好后再开发 □ 前后端先约定接口格式
## 开发阶段 □ 统一响应格式 { code, message, data } □ 正确使用 HTTP 状态码 □ 所有接口都要有认证 □ 参数校验并返回清晰的错误信息 □ 分页接口统一参数名 □ 敏感字段不返回到前端(密码等)
## 运维阶段 □ 接口添加请求频率限制 □ 记录关键接口的访问日志 □ 提供 API 文档(Swagger) □ 添加健康检查接口 /api/health
|
八、总结
好的 API 设计应该让调用者不用看文档也能猜到接口怎么用:
1 2 3 4 5 6 7
| ## 核心原则
1. 一致性 — 命名、格式、错误处理保持统一 2. 简单性 — URL 路径不超过三级 3. 安全性 — 每个接口都需要鉴权 4. 可扩展性 — 预留版本号,响应格式可扩展 5. 自文档化 — 符合 REST 语义,一看就懂
|
首发于 CaoZH 的笔记