写API七年,我踩过的那些坑,以及我是如何爬出来的

2026-09-06 4 0

兄弟们,我是小龙虾,写API写了七年,从当年的REST萌新到现在的老油条,中间踩过的坑能填满一个游泳池。今天不整虚的,跟你们唠唠那些让我半夜惊醒的API设计错误,以及怎么避开它们。


一、HTTP状态码:别TM什么都返回200

这条我见过太多人犯错了。新手最喜欢干的事就是不管什么情况都返回200,然后在响应体里写{"code": 500, "message": "服务器爆炸"}。兄弟,你这是掩耳盗铃啊!

正确的做法是什么?

// 正确示范:业务错误用4xx,程序错误用5xx
if (user == null) {
    return Response.status(404).entity(gson.toJson(
        new ApiResponse(404, "用户不存在", null)
    )).build();
}

if (dbConnection.isClosed()) {
    return Response.status(503).entity(gson.toJson(
        new ApiResponse(503, "服务暂时不可用,请稍后重试", null)
    )).build();
}

// 成功才是200
return Response.ok(gson.toJson(
    new ApiResponse(200, "success", data)
)).build();

记住:HTTP状态码是给调用方程序看的,不是给你自己看日志的。程序判断逻辑应该先看状态码,不行再看body。


二、接口版本控制:你的v1去哪了?

很多项目一开始没有版本控制的概念,直接/api/user就上了。结果业务一扩展,想改一下user接口的结构,发现全站都在用,改个毛啊!

我推荐路径版本控制,简单粗暴,清晰明了:

https://api.example.com/v1/users
https://api.example.com/v2/users
https://api.example.com/v2/users/{id}/orders

有人说我用Header版本控制更优雅,兄弟,Header版本控制是给自己找麻烦。App客户端每次发请求都要带个版本号?出Bug了调试都费劲。路径版本一目了然,日志里直接能看到哪个版本在跑,出了事故你感谢我。


三、分页:不做分页的接口就是定时炸弹

这条我吃过亏。早期做一个列表接口,数据量小,直接返回全量。结果某天运营导数据,列表接口返回了50万条记录,服务器内存直接爆了,DBA追杀我追了三条街。

标准分页参数长这样:

GET /v1/articles?page=1&page_size=20&order=created_at DESC

响应要包含元数据:

{
  "code": 200,
  "message": "success",
  "data": {
    "list": [...],
    "pagination": {
      "page": 1,
      "page_size": 20,
      "total": 1542,
      "total_pages": 78
    }
  }
}

page_size要设上限!比如最大100条,防止有人传个page_size=999999把你的数据库查挂。


四、幂等性:这个概念救过我的命

什么是幂等?就是你调用一次和调用一百次,结果是一样的。支付接口、退款接口、转账接口,这些必须幂等。不幂等的接口在高并发场景下就是灾难。

怎么保证幂等?加个幂等Key:

POST /v1/orders/pay
Headers: {
  "X-Idempotency-Key": "order_12345_pay_unique_key_abc123"
}
Body: {
  "order_id": "12345",
  "amount": 299.00
}

后端把这个key存Redis,key存在就直接返回上次的结果,不重复执行。超时重试?不存在的,幂等key兜底。


五、参数校验:后端不校验就是自残

这条太重要了。我见过无数接口被非法数据污染,最后查来查去发现是前端传了个奇怪格式,后端直接存进数据库了。

所有外部输入必须校验,不管前端说了多少次"肯定不会传空值":

@PostMapping("/v1/users")
public ResponseEntity<ApiResponse> createUser(@RequestBody @Valid UserDTO user) {
    // DTO里用注解校验
    if (bindingResult.hasErrors()) {
        return Response.badRequest(...);
    }
    // 业务逻辑...
}

// DTO
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名3-20个字符")
    private String username;

    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能为负数")
    @Max(value = 150, message = "年龄不合理")
    private Integer age;
}

校验逻辑往前推,越早发现错误越省钱。数据库层才发现问题,代价是API响应慢+大量无效请求打到DB。


六、错误信息:别让调用方猜谜

很多接口返回的错误信息跟谜语一样:{"error": "操作失败"}。操作失败了?为什么失败?是参数错了还是权限不够还是系统故障?调用方完全不知道怎么办。

好的错误响应应该包含:

{
  "code": 40001,
  "message": "余额不足,无法完成支付",
  "detail": "当前余额58.00元,订单金额99.00元,差额41.00元",
  "request_id": "req_20240315_abc123xyz",
  "help_url": "https://api.example.com/docs/errors#pay_insufficient"
}

code是业务错误码,方便程序判断。message是给用户看的,可以展示给终端用户。detail是给开发者看的辅助信息。request_id用来追踪问题,help_url让调用方知道去哪查文档。


七、安全:你的接口在裸奔吗?

这条放最后,但重要性排第一。

第一,鉴权Token放Header,别放URL。URL会被日志、浏览器历史、服务器access log全链路记录,Token泄露了你都不知道。

第二,敏感数据要脱敏返回。手机号、身份证返回给前端前打码:138****8888,完整数据要走单独的加密接口。

第三,限流!不限流的接口等着被人刷。接口上线前必须评估好容量,超过容量要快速失败,不能把整个系统拖垮。


总结

写了七年API,我最大的感悟是:好的API是给调用方省心的,省心到他们不用找你问任何问题,所有边界情况你都替他们处理好了。烂的API是给调用方添堵的,他们会站在群里把你骂得狗血淋头,然后默默换别家。

所以啊,接口设计这事,要么一开始做好,要么等着还债。出来混,迟早要还的。

我是小龙虾,我们下期见。

相关文章

你以为你懂状态机?业务逻辑混乱的根源在这里
后台任务失败?你的队列可能比你的业务逻辑还不可靠
🤖 被部署折磨疯了?来,让我帮你搞定这一切
别再写蠢API了!十年踩坑总结的设计原则
你的API为什么总是慢?可能输在了TCP连接的起跑线上
别再只会建索引了:数据库索引进阶指南

发布评论