什么是RESTful

  • RESTful(Representational State Transfer)是一种基于HTTP协议设计网络应用程序接口(API)的架构风格,由Roy Fielding在2000年的博士论文中提出。其核心思想是以资源为中心,通过统一的接口对资源进行操作,实现客户端与服务器的解耦
  • 也就是RESTful是基于http定义了一组接口格式规范,用来规范所有http请求

RESTful 设计原则

核心原则

  • 资源导向(Resource-Based)
  • 所有事物抽象为资源(如用户、订单、文章),每个资源有唯一的标识符(URI,如 /users/123)。
  • 资源通过JSON与客户端交互。
  • 统一接口(Uniform Interface)
  • 定义了HTTP方法明确操作类型:
    GET:获取资源
    POST:创建资源
    PUT:更新资源(全量替换)
    PATCH:更新资源(部分修改)
    DELETE:删除资源
  • HTTP状态码标识结果
    如200 OK、404 Not Found等。
  • 无状态(Stateless)
  • 每个请求必须包含所有必要信息,服务器不保存客户端状态(如Session),提升扩展性和可靠性。
  • 可缓存(Cacheable)
  • 响应需明确是否可缓存(如通过Cache-Control头),减少重复请求
  • 客户端无需关注服务器架构细节(如负载均衡、代理层)
  • URI命名
  • 使用名词复数表示资源(如/users而非/getUser)。
  • 避免动词,用HTTP方法表达操作(如DELETE /users/123)。
  • 层级关系用/分隔(如/users/123/orders)。
  • URI中体现版本:/api/v1/users

RESTful 接口类型

GET

  • 用途:获取资源(查询数据)。
  • 特性:
    安全:不会修改服务器资源。
    幂等:多次请求结果相同。
    可缓存:响应可被浏览器或代理缓存。
  • 响应状态码:
    200 OK(成功)
    404 Not Found(资源不存在)
    304 Not Modified(缓存未更新)
GET /users/123         # 获取ID为123的用户信息
GET /users?role=admin  # 过滤角色为admin的用户

POST

  • 用途:创建新资源(或提交非幂等操作)。
  • 特性:
    非安全:会修改服务器资源。
    非幂等:多次调用可能产生不同结果(如重复提交订单)。
  • 响应状态码:
    201 Created(成功创建,响应头包含 Location: /users/456)
    400 Bad Request(请求数据不合法)
    409 Conflict(资源冲突,如重复创建)
POST /users  
Body: { "name": "Alice", "age": 30 }  # 创建新用户

PUT

  • 用途:全量更新资源(替换整个资源)。
  • 特性:
    幂等:多次调用结果一致(如重复更新同一数据,最终状态相同)。
    若资源不存在,可创建(需由服务端决定是否支持)。
  • 响应状态码:
    200 OK 或 204 No Content(更新成功)
    201 Created(资源被创建)
    404 Not Found(资源不存在且服务端不支持创建)
PUT /users/123  
Body: { "name": "Bob", "age": 25 }  # 替换用户123的所有字段

DELETE

  • 用途:删除资源。
  • 特性:
    幂等:删除后再次调用无效果。
  • 响应状态码:
    204 No Content(删除成功,无返回内容)
    404 Not Found(资源不存在)
    403 Forbidden(无权删除)
DELETE /users/123  # 删除ID为123的用户

PATCH

  • 用途:部分更新资源(仅修改指定字段)。
  • 特性:
    非幂等:连续相同请求可能导致不同结果(如递增计数器)。
    需明确传递修改的字段。
  • 响应状态码:
    200 OK 或 204 No Content
    400 Bad Request(修改格式错误)
    404 Not Found
  • 目前Patch一般有两个标准,分别是
  • JSON Patch
  • JSON Merge Patch

JSON Patch (RFC 6902)

  • 请求体:一个由操作对象组成的 JSON 数组。
  • 操作对象:每个对象必须包含一个 op 字段指定操作类型,一个 path 字段指定目标位置,以及可能需要的 value
  • 常用操作:
  • add: 添加新值或替换现有值。
  • remove: 移除一个值。
  • replace: 替换一个值(相当于 remove + add)。
  • move: 从一个位置移动值到另一个位置。
  • copy: 从一个位置复制值到另一个位置。
  • test: 测试目标位置的值是否等于给定值(用于乐观锁验证)。
PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/email", "value": "new.email@example.com" },
  { "op": "add", "path": "/address/city", "value": "Berlin" },
  { "op": "remove", "path": "/nickname" },
  { "op": "test", "path": "/version", "value": 7 } // 确保版本号是7,否则操作失败
]
  • 优点:
  • 功能强大:支持添加、删除、替换、移动、复制和测试。
  • 操作原子性:所有操作在一个请求中顺序执行,如果任何操作失败(例如 test 失败),整个 Patch 都不会被应用。
  • 精确控制:可以操作数组中的特定元素(例如 { "op": "replace", "path": "/tags/3", "value": "Berlin" }, 表示更改第四个标签也就是第四个个元素)。
  • 缺点:
  • 学习成本:语法相对复杂,需要客户端构造操作数组。
  • 可读性稍差:不如 Merge Patch 直观。
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPatch("{id}")]
    public IActionResult Patch(int id, [FromBody] JsonPatchDocument<Product> patchDoc)
    {
        if (patchDoc == null)
        {
            return BadRequest("Patch document is null.");
        }

        // 查找要更新的产品
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product == null)
        {
            return NotFound();
        }

        // 在应用 Patch 之前记录原始状态,以便错误处理或审计
        var originalProduct = JsonConvert.SerializeObject(product);

        // 应用 Patch 操作到找到的产品实体
        patchDoc.ApplyTo(product, ModelState);

        // 检查 ModelState 是否有效
        // ApplyTo 方法会自动验证 Patch 操作本身(如路径是否存在)
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // (可选)进行模型层面的验证(如数据注解)
        if (!TryValidateModel(product))
        {
            return BadRequest(ModelState);
        }

        // 在实际应用中,这里应保存到数据库
        // _context.SaveChanges();

        // 返回更新后的实体
        return Ok(product);
    }
}

Json Patch 乐观锁

  • 乐观锁是一种并发控制策略,它假设多个事务在大多数情况下不会互相干扰。因此,它不会在整个事务过程中锁定资源,而是在提交更新时检查数据是否被其他事务修改过。如果没有,则提交成功;如果已被修改,则提交失败并通常要求客户端重试。test 操作完美地实现了这一“检查”步骤。
  • 典型工作流程:
  • 客户端A和B 获取资源 /users/123,
  • 客户端A注意到 version 字段的值为 5。
  • 客户端B 也获取了同一个资源,同样看到 version: 5。
json
GET /users/123
{"id": 123, "name": "Alice", "email": "alice@example.com", "version": 5}
  • 客户端A 想要修改邮箱。它构造一个 JSON Patch 请求,在修改之前先测试 version 是否仍然是 5。
http
PATCH /users/123 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "test", "path": "/version", "value": 5 },
  { "op": "replace", "path": "/email", "value": "alice.new@example.com" },
  { "op": "replace", "path": "/version", "value": 6 }
]
  • 假设在此期间没有其他修改:服务器接收到请求。
  • 首先执行 test:检查 /version 的值是否为 5。是,测试通过。
  • 然后执行 replace:将 email 改为新地址。
  • 最后执行 replace:将 version 递增为 6。
  • 返回成功 (200 OK)。
  • 现在客户端B想修改名字。它也用旧的版本号 5 构造了一个 Patch 请求。
http
PATCH /users/123 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "test", "path": "/version", "value": 5 },
  { "op": "replace", "path": "/name", "value": "Bob" },
  { "op": "replace", "path": "/version", "value": 6 }
]
  • 并发冲突发生! 服务器接收到客户端B的请求:
  • 首先执行 test:检查 /version 的值是否为 5。但现在的值已经是 6(由客户端A更新)。测试失败!
  • 服务器立即停止处理这个 Patch 数组,后续的 replace 操作都不会执行。
  • 数据库中的资源没有任何改变。
  • 服务器返回 412 Precondition Failed 错误给客户端B。
  • 客户端B 收到 412 错误后,其工作流程应该是:
  • 重新 GET /users/123 获取最新的资源状态(包括 name: “Alice” 和 version: 6)。
  • 基于新数据重新进行它想要的修改(例如合并名字的更改)。
  • 重新构造并发送一个新的 PATCH 请求,这次测试 version 是否为 6。
json
[
  { "op": "test", "path": "/version", "value": 6 },
  { "op": "replace", "path": "/name", "value": "Bob" },
  { "op": "replace", "path": "/version", "value": 7 }
]

JSON Merge Patch (RFC 7396)

  • 请求体:一个包含需要更新的字段及其新值的 JSON 对象。
  • 规则:
  • 提供的字段会被更新或添加。
  • 如果请求体中某个字段被显式设置为 null,则该字段会被删除。
  • 未在请求体中出现的字段保持不变
PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/merge-patch+json

{
  "email": "new.email@example.com",
  "address": {
    "city": "Berlin"
  },
  "nickname": null // 删除 nickname 字段
}
  • 优点:
  • 简单直观:客户端只需发送想要修改的字段,语法简单。
  • 易于理解:请求体看起来就像是资源的一个片段。
  • 缺点:
  • 功能有限:无法操作数组中的特定元素。要更新数组,通常必须发送整个新数组。
  • 无法区分 null 和删除:设置 null 即表示删除,如果你的业务逻辑中 null 是一个有效值,这会带来问题。
  • 没有原子性保证:不像 JSON Patch 有 test 操作来支持并发控制
//Merge Patch 没有标准实现,
//需要手动处理或使用第三方库。这里展示手动合并的方式。
[HttpPatch("merge-patch/{id}")]
public async Task<IActionResult> PatchUserMergePatch(int id, [FromBody] User partialUpdate)
{
    // 1. 获取现有用户
    var existingUser = await _userRepository.GetByIdAsync(id);
    if (existingUser == null)
    {
        return NotFound();
    }

    // 2. 手动合并逻辑
    // 更新提供的字段,忽略 null 字段(或者根据业务逻辑,将 null 视为删除)
    if (partialUpdate.Email != null)
    {
        existingUser.Email = partialUpdate.Email;
    }
    // 注意:对于值类型(int, bool等),无法区分“未提供”和“提供了默认值”
    // ... 处理其他字段

    // 3. 验证和保存
    if (!TryValidateModel(existingUser))
    {
        return BadRequest(ModelState);
    }

    await _userRepository.UpdateAsync(existingUser);
    return Ok(existingUser);
}

RESTful 状态码

1xx(信息响应)

  • 100 Continue
    客户端应继续发送请求的剩余部分(用于大文件上传前的确认)

2xx(成功)

  • 200 OK: 请求成功,
    返回响应数据(如 GET 或 PUT 请求成功)。
  • 201 Created
    资源创建成功(如 POST 请求后返回新资源的URI,响应头包含 Location: /users/123)。
  • 202 Accepted
    请求已接受但未处理完成(常用于异步任务)。
  • 204 No Content
    请求成功,但无返回内容(如 DELETE 成功或 PUT 更新后无需返回数据)。

3xx(重定向)

  • 301 Moved Permanently
    资源永久迁移到新URI(需更新书签)。
  • 302 Found
    资源临时重定向到新URI(浏览器默认行为是后续请求仍用原URI)。
  • 304 Not Modified
    资源未修改,客户端可使用缓存(配合 If-Modified-Since 头使用)。

4xx(客户端错误)

  • 400 Bad Request
    请求格式错误(如参数缺失或JSON语法错误)。
  • 401 Unauthorized
    未认证(需提供有效身份凭证,如JWT失效)。
  • 403 Forbidden
    已认证但无权限访问资源(如普通用户访问管理员接口)。
  • 404 Not Found
    资源不存在(URI无效或资源已删除)。
  • 405 Method Not Allowed
    HTTP方法不支持(如对只允许 GET 的URI发送 POST)。
  • 409 Conflict
    资源状态冲突(如重复创建同名用户)。
  • 429 Too Many Requests
    请求频率超出限制(常见于API限流)。

5xx(服务器错误)

  • 500 Internal Server Error
    服务器内部错误(通用兜底错误,需排查日志)。
  • 502 Bad Gateway
    网关或代理服务器收到无效响应(如后端服务崩溃)。
  • 503 Service Unavailable
    服务暂时不可用(如维护或过载,可配合 Retry-After 头提示重试时间)。
  • 504 Gateway Timeout
    网关或代理服务器超时(后端服务未及时响应)

RESTful Uri设计原则

对于Api的uri命名要遵守以下设计

  • 使用名词复数表示资源(如/users而非/getUser)。
  • 避免动词,用HTTP方法表达操作
  • 如DELETE /users/123,而不是DELETE /delete-users/123)。
  • 层级关系用/分隔
  • 如/users/123/orders, 而不是 /users/orders/123
  • URI中体现版本:/api/v1/users
  • 推荐使用小驼峰,比如/pet/petId
  • uri应该大小写敏感,并且推荐使用全小写
  • 以上只是建议,具体根据实际情况而定

Example: https://petstore.swagger.io/

  • 以下Api来住swagger官方例子,可以看到其实并没有严格遵守规范,比如Api里没有版本信息,命名里有动词等等
    在这里插入图片描述

Api传参:QueryString 和 UriPath

使用 URI Path(路径参数)的场景

  • URI Path 用于标识 资源的主键 或 资源间的层级关系,通常对应服务端的 核心数据模型。

适用场景:

  • 唯一资源标识
    通过路径参数直接定位唯一资源(如用户ID、订单ID)。
GET /users/{id}          # 获取指定ID的用户
DELETE /orders/{orderId} # 删除指定ID的订单
  • 资源层级关系
    表示资源之间的从属关系(如用户的所有订单)。
    示例:
GET /users/{userId}/orders  # 获取用户的订单列表

特点:

  • 必要性:路径参数通常是 必填 的(缺少会导致404)。
  • 语义明确:路径体现资源的唯一性和结构(如 /users/123 明确指向用户123)。
  • 缓存友好:路径参数不同的 URL 会被视为不同资源,适合缓存(如 CDN 缓存)

使用 Query String(查询参数)的场景

  • Query String 用于 过滤、排序、分页 或 附加操作,通常是 可选 的非核心参数。

适用场景:

  • 过滤数据
    筛选符合特定条件的资源列表。
示例:
GET /users?role=admin&status=active  # 获取角色为admin且状态为活跃的用户
  • 分页与排序
    控制返回数据的范围和顺序。
示例:
GET /articles?page=2&limit=20&sort=-created_at  # 获取第二页文章,按创建时间倒序
  • 附加操作
    触发特定功能(如统计、导出格式)。
示例:
GET /reports/sales?format=csv     # 导出CSV格式的销售报表
GET /products?fields=name,price  # 仅返回名称和价格字段(字段选择)
  • 非资源标识参数
    不影响资源定位,仅修饰响应结果。
示例:
GET /search?q=restful&lang=en  # 搜索关键词"restful",语言为英文

特点:

  • 可选性:缺少查询参数时,通常返回默认结果(如不分页)。
  • 灵活性:参数组合自由,适合动态需求(如多种过滤条件)。
  • 幂等性:同一 Query String 的 GET 请求是幂等的(结果可缓存)

RESTful和HTTP的区别

  • RESTful并不等于HTTP,它只是基于HTTP的一些特性设定了一组设计规范
  • 类似的还有GraphQL,也是基于HTTP的特性设定了一组规范

注意事项

  • 以上只是标准的设计规范,并没有技术上的限制,开发者完全可以POST当PUT用,PUT当POST用,甚至POST当GET用等等,只是这样不规范,会导致程序可维护性降低
  • 现实开发中需要根据实际情况调整,最好和以前的开发风格保持一致
  • 大部分Api的设计都没有严格的遵守以上规则,比如Api的uri里没有版本信息,返回的状态码用的不准确等,使用动词等等
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐