第四章 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 设计这块,说白了就是「约定大于配置」。把规矩定好了,前后端协作起来就顺畅多了。下一章咱们聊聊数据访问层,那又是另一个故事了。