第四章 RESTful API 设计:HTTP 动词与状态码、路由设计、请求响应模型、版本控制策略
好,咱们今天聊聊 RESTful API 设计。说实话,这玩意儿我刚开始接触的时候也觉得挺玄乎的。不就是写几个接口嘛,能有多大学问?后来被坑了几次才明白——API 设计得好不好,直接决定了后端代码能活多久。
4.1 HTTP 动词与状态码:别乱用,有讲究
RESTful 的核心思想,说白了就是把资源暴露出来,然后用 HTTP 动词去操作它。我见过太多项目,所有接口全用 POST,增删改查全靠请求体里的一个字段区分。嗯,这种写法,维护起来真的想哭。
4.1.1 动词的选择
每个动词都有它该干的事:
- GET:查资源。幂等的,不改变服务器状态。
- POST:创建资源。不幂等,每次调用可能产生新数据。
- PUT:全量更新。幂等的,你传什么我就替换成什么。
- PATCH:部分更新。只改你传过来的字段。
- DELETE:删资源。幂等的,删一次和删十次结果一样。
重要原则:动词只描述动作,不描述资源。资源永远用名词复数。
举个例子,获取用户列表:
// 好的写法
GET /api/users
// 坏的写法
GET /api/getUsers
POST /api/users/getAll
为什么会这样?因为 /getUsers 把动作写进了 URL,这就违背了 RESTful 的初衷。我在项目中遇到过,有人把 /api/deleteUserById 写成 GET 请求,结果爬虫一跑,用户全没了……
4.1.2 状态码:别只用 200 和 500
很多新手喜欢所有成功都返回 200,所有失败都返回 500。这其实是个大坑。前端拿到 500,根本不知道是参数错了、没权限了、还是服务器炸了。
我建议你这样用:
| 场景 | 状态码 | 说明 |
|---|---|---|
| 查询成功 | 200 OK | 返回数据 |
| 创建成功 | 201 Created | 返回新资源 ID |
| 删除成功 | 204 No Content | 不返回 body |
| 参数错误 | 400 Bad Request | 比如缺少必填字段 |
| 未授权 | 401 Unauthorized | 没登录 |
| 无权限 | 403 Forbidden | 登录了但没权限 |
| 资源不存在 | 404 Not Found | 查不到数据 |
| 服务器异常 | 500 Internal Server Error | 代码炸了 |
小技巧:我曾经在项目里统一封装了一个 ApiResult 类,所有接口都返回这个结构。状态码用 HTTP 标准,业务错误码自己定义。前端只看 HTTP 状态码就知道该走哪个分支,省心不少。
4.2 路由设计:Attribute Routing 是真香
在 ASP.NET Core 里,路由有两种玩法:Convention Routing 和 Attribute Routing。我个人习惯用 Attribute Routing,为什么?因为它把路由和控制器绑在一起,看代码就知道这个接口是干嘛的。
4.2.1 基本用法
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
// 查用户
return Ok(user);
}
[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
// 创建用户
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
[HttpPut("{id}")]
public IActionResult UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
// 全量更新
return NoContent();
}
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id)
{
// 删除
return NoContent();
}
}
你看,每个方法上都标明了 HTTP 动词和路由参数,一目了然。你想想看,如果换成 Convention Routing,你得去 Startup.cs 里翻半天才知道路由长什么样。
4.2.2 路由参数与约束
路由里可以带参数,还能加约束:
[HttpGet("{id:int:min(1)}")]
public IActionResult GetUser(int id) { }
[HttpGet("{name:alpha}")]
public IActionResult GetUserByName(string name) { }
[HttpGet("{date:datetime}")]
public IActionResult GetByDate(DateTime date) { }
注意:约束写错了,路由就匹配不上。我曾经在项目里把 int 写成了 integer,结果接口一直 404,查了半天才发现是约束名写错了……
4.3 请求与响应模型:别把数据库实体直接往外扔
这个坑我踩过。刚开始做项目的时候,图省事,直接把 Entity Framework 的实体类当成 API 的返回模型。结果呢?
- 用户密码字段暴露了
- 导航属性循环引用,序列化报错
- 前端要的字段名和数据库字段名对不上
所以,我建议你这样做:
4.3.1 请求模型(DTO)
public class CreateUserRequest
{
[Required]
[MaxLength(50)]
public string UserName { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(6)]
public string Password { get; set; }
}
用 [FromBody] 接收,框架自动做模型验证。验证不通过,直接返回 400,省得你写一堆 if 判断。
4.3.2 响应模型
public class UserResponse
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
// 不返回 Password
}
核心原则:请求模型只包含输入字段,响应模型只包含输出字段。永远不要混用。
4.4 版本控制策略:别让旧客户端炸了
API 上线后,难免要改。但你不能让正在用的 App 突然挂掉。所以,版本控制是必须的。
常用的策略有三种:
4.4.1 URL 路径版本
/api/v1/users
/api/v2/users
最简单粗暴,也最直观。我早期项目都用这个。缺点是 URL 里带版本号,看着有点丑。
4.4.2 请求头版本
GET /api/users
Headers: api-version=1.0
URL 干净了,但调试的时候得多传一个头。适合对 URL 洁癖比较重的团队。
4.4.3 查询字符串版本
/api/users?version=1.0
介于两者之间。缓存策略会麻烦一点,因为同一个 URL 可能对应不同版本的数据。
在 ASP.NET Core 里,用 Microsoft.AspNetCore.Mvc.Versioning 包可以轻松实现:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase { }
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase { }
我的建议:小项目用 URL 路径版本,简单直接。大项目用请求头版本,配合 Swagger 文档,前端可以自由选择版本。我曾经在一个项目里同时维护了 3 个版本,全靠版本控制策略才没乱套。
4.5 避坑指南
- 别把动词写进 URL:
/api/getUsers是坏味道,用GET /api/users。 - 状态码别乱用:200 表示成功,201 表示创建成功,204 表示删除成功。别所有成功都返回 200。
- DTO 是必须的:别偷懒直接用实体类,迟早会出事。
- 版本控制要趁早:等客户端上线了再想加版本,那就晚了。
嗯,RESTful API 设计这块,说白了就是「约定大于配置」。把规矩定好了,前后端协作起来就顺畅多了。下一章咱们聊聊数据访问层,那又是另一个故事了。