Skip to content

RESTful API 设计规范

你有没有见过这样的接口:

GET /api/getUser?id=1
POST /api/createUser
POST /api/updateUser
GET /api/deleteUser?id=1

这不是 RESTful,这是「REST 风格的 CRUD」——看起来像 REST,但其实没有理解 REST 的精髓。

真正的 RESTful API 应该是什么样的?让我们一起来探索。

什么是 REST

REST(Representational State Transfer)是一种软件架构风格,不是标准或协议。它的核心思想是:

将所有内容抽象为资源,资源通过 URI 标识,通过 HTTP 动词操作资源。

关键词是「资源」和「动词」。

RESTful 设计六大原则

原则说明
1. 客户端-服务器分离客户端和服务器独立发展,互不影响
2. 无状态服务器不保存客户端状态,每次请求包含所有信息
3. 可缓存响应可以被缓存,提高性能
4. 分层系统允许中间层(网关、负载均衡)存在
5. 统一接口资源通过 URI 标识,操作通过 HTTP 方法
6. 按需代码(可选)服务器可以发送可执行代码给客户端

资源命名

核心规则

资源使用名词,不是动词。

java
// 错误:使用动词
GET  /api/getUser?id=1
POST /api/createUser
POST /api/updateUser
GET  /api/deleteUser?id=1

// 正确:使用名词
GET    /api/users/1       // 获取用户
POST   /api/users         // 创建用户
PUT    /api/users/1       // 更新用户
DELETE /api/users/1       // 删除用户

复数还是单数?

业界没有统一标准,但推荐使用复数

java
// 推荐:复数形式
GET /api/users      // 获取用户列表
GET /api/users/1    // 获取单个用户

// 也可以:单数形式(用于全局操作)
POST /api/user/login
POST /api/user/logout

层级结构

java
// 用户下的订单
GET /api/users/1/orders

// 订单下的商品
GET /api/users/1/orders/100/items

查询参数 vs 路径参数

java
// 路径参数:用于标识特定资源
GET /api/users/1           // 获取 ID 为 1 的用户
GET /api/orders/100        // 获取订单号 100

// 查询参数:用于过滤、排序、分页
GET /api/users?status=active&page=1&size=20
GET /api/users?sort=createdAt,desc&fields=id,name,email

HTTP 方法

方法语义幂等性安全性典型用途
GET查询幂等安全获取资源
POST创建非幂等不安全创建资源
PUT完整更新幂等不安全更新资源(全部字段)
PATCH部分更新非幂等不安全更新资源(部分字段)
DELETE删除幂等不安全删除资源
HEAD获取元数据幂等安全检查资源是否存在
OPTIONS获取支持的方法幂等安全CORS 预检

GET - 资源查询

java
// 获取所有用户
GET /api/users

// 获取单个用户
GET /api/users/123

// 获取用户下的订单
GET /api/users/123/orders

// 搜索用户(复杂查询用查询参数)
GET /api/users?username=张三&age_gt=18

POST - 创建资源

java
// 创建用户
POST /api/users
Content-Type: application/json

{
    "username": "zhangsan",
    "email": "zhangsan@example.com",
    "password": "123456"
}

// 返回 201 Created
// Location: /api/users/124

PUT - 完整更新

java
// 完整更新用户(所有字段)
PUT /api/users/123
Content-Type: application/json

{
    "username": "zhangsan_updated",
    "email": "zhangsan_new@example.com",
    "password": "654321",
    "age": 25,
    "status": "active"
}

PATCH - 部分更新

java
// 部分更新(只更新需要的字段)
PATCH /api/users/123
Content-Type: application/json

{
    "email": "new_email@example.com"
}

// 或者
PATCH /api/users/123
Content-Type: application/json

{
    "op": "replace",
    "path": "/email",
    "value": "new_email@example.com"
}

DELETE - 删除资源

java
// 删除用户
DELETE /api/users/123

// 返回 204 No Content

状态码

2xx - 成功

状态码含义使用场景
200 OK成功GET、PUT、PATCH 成功
201 Created已创建POST 创建新资源成功
202 Accepted已接受异步操作,请求已接受但未完成
204 No Content无内容DELETE 成功,无返回内容

3xx - 重定向

状态码含义使用场景
301 Moved Permanently永久重定向资源已迁移到新地址
302 Found临时重定向临时跳转到其他 URI
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网关超时上游服务响应超时

统一响应格式

成功响应

json
{
    "code": 200,
    "message": "success",
    "data": {
        "id": 123,
        "username": "zhangsan",
        "email": "zhangsan@example.com"
    },
    "timestamp": 1699000000000
}

列表响应(带分页)

json
{
    "code": 200,
    "message": "success",
    "data": {
        "list": [
            {"id": 1, "username": "user1"},
            {"id": 2, "username": "user2"}
        ],
        "pagination": {
            "page": 1,
            "size": 20,
            "total": 100,
            "totalPages": 5
        }
    }
}

错误响应

json
{
    "code": 400,
    "message": "参数校验失败",
    "errors": [
        {"field": "email", "message": "邮箱格式不正确"},
        {"field": "password", "message": "密码长度不能少于 6 位"}
    ],
    "timestamp": 1699000000000,
    "traceId": "abc123"
}

Spring MVC 实现

Controller 示例

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 获取用户列表
    @GetMapping
    public Result<PageResult<UserVO>> list(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) String status) {
        
        PageResult<UserVO> result = userService.list(page, size, status);
        return Result.success(result);
    }
    
    // 获取单个用户
    @GetMapping("/{id}")
    public Result<UserVO> getById(@PathVariable Long id) {
        UserVO user = userService.findById(id);
        if (user == null) {
            return Result.error(404, "用户不存在");
        }
        return Result.success(user);
    }
    
    // 创建用户
    @PostMapping
    public Result<UserVO> create(@RequestBody @Valid CreateUserRequest request) {
        UserVO user = userService.create(request);
        return Result.success(user)
            .withStatus(HttpStatus.CREATED)
            .withHeader("Location", "/api/users/" + user.getId());
    }
    
    // 完整更新用户
    @PutMapping("/{id}")
    public Result<UserVO> update(@PathVariable Long id,
                                   @RequestBody @Valid UpdateUserRequest request) {
        UserVO user = userService.update(id, request);
        return Result.success(user);
    }
    
    // 部分更新用户
    @PatchMapping("/{id}")
    public Result<UserVO> patch(@PathVariable Long id,
                                 @RequestBody UpdateUserRequest request) {
        UserVO user = userService.patch(id, request);
        return Result.success(user);
    }
    
    // 删除用户
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success(null).withStatus(HttpStatus.NO_CONTENT);
    }
}

请求对象

java
// 创建用户请求
public class CreateUserRequest {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度为 3-20 位")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度不能少于 6 位")
    private String password;
    
    private Integer age;
    
    private MultipartFile avatar;
}

// 更新用户请求
public class UpdateUserRequest {
    @Size(min = 3, max = 20, message = "用户名长度为 3-20 位")
    private String username;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    private Integer age;
    
    private String status;
}

高级特性

批量操作

java
// 批量创建
POST /api/users/batch
[
    {"username": "user1", "email": "user1@example.com"},
    {"username": "user2", "email": "user2@example.com"}
]

// 批量更新
PUT /api/users/batch
[
    {"id": 1, "status": "active"},
    {"id": 2, "status": "inactive"}
]

// 批量删除
DELETE /api/users/batch?ids=1,2,3

关联资源操作

java
// 获取用户的所有订单
GET /api/users/{userId}/orders

// 为用户添加订单
POST /api/users/{userId}/orders

// 获取用户的指定订单
GET /api/users/{userId}/orders/{orderId}

HATEOAS(超媒体)

返回资源的同时,提供相关链接:

json
{
    "id": 1,
    "username": "zhangsan",
    "_links": {
        "self": {"href": "/api/users/1"},
        "orders": {"href": "/api/users/1/orders"},
        "profile": {"href": "/api/users/1/profile"}
    }
}

Spring HATEOAS 使用:

java
@GetMapping("/{id}")
public EntityModel<UserVO> getUser(@PathVariable Long id) {
    UserVO user = userService.findById(id);
    
    return EntityModel.of(user,
        linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
        linkTo(methodOn(UserController.class).getOrders(id)).withRel("orders")
    );
}

版本管理

URL 版本

java
// v1 版本
GET /api/v1/users

// v2 版本(接口不兼容时)
GET /api/v2/users

Header 版本

java
// 请求头
GET /api/users
Accept: application/vnd.api.v2+json

RESTful 反模式

反模式问题正确做法
/getUser路径中包含动词使用 HTTP GET 方法
/users?action=delete用查询参数做操作使用 DELETE 方法
/users/123/update路径中包含动词使用 PUT/PATCH 方法
所有请求都返回 200不区分成功失败使用正确的状态码
不返回 Location 头创建资源后无法获取 URI返回 201 并带 Location
暴露内部错误信息安全风险日志记录内部错误,外部返回通用信息

面试追问

Q1: PUT 和 PATCH 的区别?

  • PUT:完整更新,需要提供所有字段
  • PATCH:部分更新,只更新提供的字段
java
// PUT 请求需要包含所有字段
PUT /api/users/123
{"username": "new", "email": "new@example.com", "age": 25, "status": "active"}

// PATCH 请求只需要包含要更新的字段
PATCH /api/users/123
{"email": "new@example.com"}

Q2: RESTful 的缺点?

  • 不适合需要长时间运行的操作(如视频处理)
  • 不适合需要实时通信的场景(如聊天)
  • 复杂查询使用 URL 可能过长

Q3: 如何处理 API 限流?

java
// 返回 429 Too Many Requests
// 并带上重试时间
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699000060

下节预告Spring 配置类 @Configuration 与 @Bean —— 深入理解 Java 配置类的原理和使用方式。

基于 VitePress 构建