4.2 RESTful API 设计与实现


4.2 RESTful API 设计与实现

上一节让请求找到了归宿,这一节回答归宿里该装什么:把订单业务建模成 REST 资源,设计资源与方法矩阵,再把矩阵落成可运行的 Koa 代码。REST 常被误解为"URL 好看一点",本节要立住的是它的实质——用 HTTP 本身的语义(方法、状态码、头)承载接口的全部约定,让客户端不需要读文档就能猜对一半。

先建模:订单是资源,操作是方法

REST 的第一步是把业务名词翻译成资源。订单系统里的名词有:订单(orders)、订单里的条目(orders 的子资源 items)。动词——创建、查询列表、查详情、改地址、取消——全部映射到统一的方法集上,不允许自造动词路径(/deleteOrder 这类设计在这一步就被拦下)。

资源与方法矩阵是这一步的交付物,对着它写代码、对着它写文档、对着它做测试:

操作 方法与路径 成功状态码 失败状态码
查询订单列表 GET /orders 200 401
分页与筛选 GET /orders?status=paid&page=2 200 401
查询订单详情 GET /orders/:id 200 401 / 404
创建订单 POST /orders 201 + Location 头 400 / 401 / 422
全量替换(罕用) PUT /orders/:id 200 400 / 404
部分更新地址 PATCH /orders/:id/address 200 400 / 404
取消订单 POST /orders/:id/cancellation 200 409 / 404
删除订单 DELETE /orders/:id 204 403 / 404

矩阵里有三处值得展开的判断。其一,取消订单用了 POST 加名词路径,而不是 PATCH 状态字段——"取消"是有独立业务规则的动作(只有待支付可取消、要触发退款流程),一个动词化的子资源比让客户端 PUT status 字段更安全也更可审计。REST 不是教条,状态迁移类动作用动作子资源表达是主流实践。其二,删除成功回 204 无内容,客户端拿到的就是"删掉了",没有废话。其三,409 Conflict 留给状态冲突(已发货的订单不可取消),与 400(参数本身不合法)、422(参数合法但语义不通过)形成梯度。

图 4-1:订单资源的方法矩阵全景

图 4-1:订单资源的方法矩阵全景

再实现:一个可运行的订单 CRUD

把矩阵落成代码。为了聚焦路由与语义,存储用内存 Map,真实项目换成数据库即可,接口层不变:

const Router = require('koa-router'); const { BadRequest, NotFound, Forbidden, Conflict } = require('../errors'); const { validate } = require('../middleware/validate'); const router = new Router({ prefix: '/api/v1/orders' }); const store = new Map(); // id -> order let seq = 1000; // 列表:支持 status 筛选与 page 分页 router.get('/', auth(), async (ctx) => { const { status, page = '1', size = '20' } = ctx.query; let list = [...store.values()].filter((o) => o.userId === ctx.state.user.id); if (status) list = list.filter((o) => o.status === status); const p = Math.max(1, parseInt(page, 10) || 1); const s = Math.min(100, Math.max(1, parseInt(size, 10) || 20)); ctx.body = { total: list.length, page: p, items: list.slice((p - 1) * s, p * s), }; }); // 详情:不存在即 404,语义由异常类携带 router.get('/:id', auth(), async (ctx) => { const order = store.get(ctx.params.id); if (!order) throw new NotFound(`订单 ${ctx.params.id} 不存在`); if (order.userId !== ctx.state.user.id) throw new Forbidden(); ctx.body = order; }); // 创建:201 + Location 头 router.post('/', auth(), validate(createSchema), async (ctx) => { const id = `ORD-${++seq}`; const order = { id, userId: ctx.state.user.id, items: ctx.validated.items, address: ctx.validated.address, status: 'pending', createdAt: new Date().toISOString(), }; store.set(id, order); ctx.status = 201; ctx.set('Location', `/api/v1/orders/${id}`); ctx.body = order; }); // 取消:动作子资源,状态冲突回 409 router.post('/:id/cancellation', auth(), async (ctx) => { const order = store.get(ctx.params.id); if (!order) throw new NotFound(); if (order.status !== 'pending') { throw new Conflict(`订单当前状态为 ${order.status},不可取消`); } order.status = 'cancelled'; order.cancelledAt = new Date().toISOString(); ctx.body = order; }); // 删除:204 无内容 router.delete('/:id', auth(), requireRole('admin'), async (ctx) => { if (!store.delete(ctx.params.id)) throw new NotFound(); ctx.status = 204; });

对照矩阵逐行核对:状态码全部来自异常类或显式赋值;创建回 201 并带 Location 让客户端能拿到新资源地址;取消是状态迁移,用动作子资源加 409 语义;删除回 204。处理器里没有一处手写 ctx.status = 4xx 的散落判断——错误语义全部委托给 2.4 节的异常体系,这正是分层的回报。

用命令行验收语义

接口写完,curl 会话就是验收单:

# 创建:201,响应头带 Location curl -i -X POST http://localhost:3000/api/v1/orders \ -H 'authorization: Bearer <token>' -H 'content-type: application/json' \ -d '{"items":[{"sku":"A1","qty":2}],"address":"上海市某路 1 号"}' # HTTP/1.1 201 Created # Location: /api/v1/orders/1001 # 查详情:200 curl -i http://localhost:3000/api/v1/orders/ORD-1001 -H 'authorization: Bearer <token>' # 用错误方法访问:405 + Allow 头 curl -i -X PUT http://localhost:3000/api/v1/orders/ORD-1001/cancellation \ -H 'authorization: Bearer <token>' # HTTP/1.1 405 Method Not Allowed # Allow: POST # 删除:204,无响应体 curl -i -X DELETE http://localhost:3000/api/v1/orders/ORD-1001 \ -H 'authorization: Bearer <token>' # HTTP/1.1 204 No Content

⚠️ 常见坑:把业务规则硬塞进 PATCH。客户端 PATCH /orders/:id 直接改 status 字段,等于把状态机交给前端随意拨动——已发货也能改成待支付。状态迁移必须走显式动作(cancellation、payment),PATCH 只留给真正的属性编辑(如收货地址)。状态机的入口收得越窄,系统的可审计性越强。

本节要点回顾

  • 建模先行:名词成资源、动词进方法集,动作类迁移用名词化子资源表达;
  • 矩阵是交付物:每个操作的路径、成功码、失败梯度在设计期写清,代码照单施工;
  • 状态码有梯度:400 参数、401 凭证、403 权限、404 不存在、409 状态冲突、422 语义不通过;
  • 创建的礼节:201 加 Location 头;删除的礼节:204 无内容;
  • 处理器只写业务:错误语义委托异常体系,散落的状态码赋值是坏味道。

矩阵里的 ?status=paid&page=2 与请求体怎么从"不可信输入"变成"可信参数"——下一节把参数解析与校验做成一层前置事实。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U