前端转 Agent 开发之后端知识 - 02. HTTP 与 API 基础

4 阅读6分钟

02. HTTP 与 API 基础

前端更关注页面和交互,后端更关注请求和响应。你做的不是“接口函数”,而是一套可长期维护的通信契约。

核心认知

  • 一个 API 的质量,不只看能不能返回数据,还看语义是否稳定、错误是否清晰、边界是否可控。
  • 后端最怕“看起来能用”,但一遇到重试、超时、重复提交就出错。

一个请求最小应该包含什么

  • 请求方法:GET / POST / PATCH / DELETE
  • 路径:资源的定位方式
  • Query:过滤、分页、排序
  • Header:鉴权、请求追踪、缓存控制
  • Body:创建或更新的负载

响应最少要考虑:

  • 状态码
  • 响应体结构
  • 错误结构
  • 是否幂等
  • 是否可缓存

必懂 6 件事

1. 方法和状态码
  • GET 用于读,POST 常用于创建,PUT/PATCH 用于更新,DELETE 用于删除。
  • 200/201/204 表示成功,400/401/403/404/409/500 要能区分场景。
  • 不要把所有失败都返回 200 再塞一个错误文案。

最常见的状态码语义:

  • 400 Bad Request:参数错误、校验失败
  • 401 Unauthorized:未登录或 token 无效
  • 403 Forbidden:已登录,但没权限
  • 404 Not Found:资源不存在
  • 409 Conflict:重复提交、状态冲突、唯一约束冲突
  • 500 Internal Server Error:服务内部异常
2. URL、分页、过滤
  • 列表接口要提前设计分页,不要默认全量返回。
  • 过滤条件要明确,例如状态、时间范围、排序字段。
  • 参数命名要稳定,不要今天 pageSize,明天 size

一个比较稳的列表接口例子:

GET /users?page=1&pageSize=20&status=active&keyword=tom&sortBy=createdAt&sortOrder=desc

响应可以保持稳定结构:

{
  "data": [
    { "id": 1, "name": "Tom" }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "total": 135
  }
}
3. 统一错误结构
  • 错误响应要有稳定的机器可读字段,而不只是给人看的提示语。
  • 最小结构可以是:
{
  "code": "USER_NOT_FOUND",
  "message": "user not found",
  "requestId": "req_123"
}

一个 Express 版的统一错误处理中间件:

import express from 'express';
import crypto from 'node:crypto';

const app = express();

app.use((req, res, next) => {
  const requestId = crypto.randomUUID();
  req.requestId = requestId;
  res.setHeader('X-Request-Id', requestId);
  next();
});

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await userService.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        code: 'USER_NOT_FOUND',
        message: 'user not found',
        requestId: req.requestId,
      });
    }

    res.json({ data: user });
  } catch (error) {
    next(error);
  }
});

app.use((error, req, res, _next) => {
  console.error(req.requestId, error);

  res.status(500).json({
    code: 'INTERNAL_ERROR',
    message: 'internal server error',
    requestId: req.requestId,
  });
});
4. 幂等性、超时、重试
  • 幂等性是后端基本功,尤其是支付、下单、回调这类操作。
  • 超时和重试不是客户端的问题,服务端也要考虑重复请求和中间状态。
  • 典型手段包括唯一索引、幂等键、状态机。

先把定义说清楚:

  • 幂等不是“请求只能发一次”
  • 幂等是“同一个业务请求重复执行多次,最终业务结果一致”

例如用户连续点两次“提交订单”,理想结果不是创建两笔订单,而是后端识别出这是同一笔业务意图,最终只保留一笔。

为什么这件事必须后端处理:

  1. 客户端超时,不代表服务端没成功
  2. 网络重试、用户重复点击、第三方回调重放,都会把同一笔业务再次打到服务端
  3. 如果后端没有幂等保护,就会出现重复下单、重复扣款、重复发券、重复扣库存

一个常见方案是要求客户端传 Idempotency-Key

POST /orders
Idempotency-Key: 75b1f5b6-0d8f-4f53-9b39-1e8ad3d247fa

服务端伪代码:

const key = req.header('Idempotency-Key');

if (!key) {
  throw new BadRequestError('missing idempotency key');
}

try {
  return await db.orders.create({
    data: { userId, idempotencyKey: key, ...payload },
  });
} catch (error) {
  if (isUniqueConflict(error)) {
    return db.orders.findUnique({
      where: { userId_idempotencyKey: { userId, idempotencyKey: key } },
    });
  }

  throw error;
}

这个方案的关键点是:

  • 客户端必须在“同一笔业务重试”时复用同一个 key
  • 后端必须把 userId + idempotencyKey 落到唯一约束里
  • 真正兜底的是数据库唯一约束,而不是“先查一下再插入”

数据库约束示例:

CREATE UNIQUE INDEX uk_orders_user_idempotency
ON orders (user_id, idempotency_key);

除了“唯一约束 + 幂等键”,还有两种常见做法:

  • 状态机:例如订单只能从 PENDING -> PAID,如果已经是 PAID,重复回调就不再处理
  • 去重表:例如 webhook 或消息消费时,先插一条唯一 event_id,插入失败就说明处理过

状态机伪代码:

UPDATE orders
SET status = 'PAID'
WHERE id = 123
  AND status = 'PENDING';

如果影响行数是 0,就表示这次更新没有真的推进状态,通常意味着已经处理过,或者状态不合法。

你还要特别注意顺序问题:

  • 幂等判断必须发生在副作用之前,或者和副作用放在同一事务里
  • 不能先扣库存,再做幂等判断;否则重复请求可能已经把库存扣了两次
  • 幂等解决的是“重复执行”,不是所有并发问题;库存、余额这类仍要配合事务、条件更新、锁

一句话记住:

  • 幂等要解决的是“同一笔业务因为重试或重复提交被执行多次时,如何保证最终只生效一次”
5. 限流与输入校验
  • 后端不能假设客户端一定传对参数。
  • 需要在系统边界做类型、格式、范围校验。
  • 对高频接口要有限流意识,避免被刷爆。

下面是一个用 zod 做 Query 校验的最小例子:

import { z } from 'zod';

const listUsersQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  status: z.enum(['active', 'disabled']).optional(),
});

app.get('/users', async (req, res, next) => {
  try {
    const query = listUsersQuerySchema.parse(req.query);
    const result = await userService.list(query);
    res.json(result);
  } catch (error) {
    next(error);
  }
});

限流伪代码:

const key = `rate-limit:${userId}`;
const count = await redis.incr(key);

if (count === 1) {
  await redis.expire(key, 60);
}

if (count > 100) {
  throw new TooManyRequestsError();
}
6. API 分层

一个健康的 API 至少分三层:

  • 路由 / Controller:接收请求、参数校验、组织响应
  • Service:业务逻辑
  • Repository / DAO:数据库访问

别把所有逻辑都写在路由里。后面一接数据库、鉴权、日志、缓存,单文件会迅速失控。

最小实践

  • 做一个 users 模块,包含列表、详情、创建、更新、删除。
  • 给列表接口加分页和过滤。
  • 所有错误都返回统一 JSON 结构。
  • 给每个请求生成 requestId,方便日志关联。

常见误区

  • 把“接口通了”当作 API 设计完成。真正难的是错误、边界和兼容性。
  • 把分页留到后面再加。线上一旦数据量起来,再补分页成本很高。
  • 只靠前端做参数校验。只要请求能直接打到服务,后端就必须自己校验。

学会的标准

  • 你能设计出一组语义清晰的 API,而不是只堆路径。
  • 你能解释为什么重复提交会带来数据问题。
  • 你能说清楚哪些错误该返回 400401403409
  • 你能写出带 requestId 和统一错误结构的最小 API 服务。