也能搞懂的RESTful API设计指南:7天写出规范后端接口(附避坑清单)

也能搞懂的RESTful API设计指南:7天写出规范后端接口(附避坑清单)

小白也能搞懂的RESTful API设计指南:7天写出规范后端接口(附避坑清单)

小白也能搞懂的RESTful API设计指南:7天写出规范后端接口

为啥你写的接口总被后端同事翻白眼?

说实话,我刚入行那会儿,写个接口能被后端老哥喷到怀疑人生。我记得特清楚,有次我写了个/getUserInfo的接口,对面直接甩过来一句话:“兄弟,你这URL咋还带上动词了?咱这是写代码还是写英语作文呢?”

当时我就懵了,心说这不挺清晰的吗,get用户info,多直白啊。结果人家给我发来一串链接,让我好好学学什么叫RESTful。我点开一看,好家伙,全是洋文,看得我头皮发麻。

后来混久了才明白,RESTful这玩意儿其实就是个约定俗成的江湖规矩。就像你去别人家做客,进门要换鞋、吃饭别吧唧嘴一样,不是硬性法律,但你不这么做就显得特没教养。接口设计也一样,你瞎写倒也能跑,但上下游对接的时候,别人一看你的文档,心里就会默默给你打上"野生程序员"的标签。

最尴尬的是啥?是你好不容易写了个功能,前端调你的接口,你返回个200,然后body里写个{"code": 500, "msg": "系统错误"}。前端小哥当场就炸了:“你HTTP状态码给200,意思是请求成功了,结果里面告诉我系统错误?你搁这儿玩套娃呢?”

所以啊,这篇文章就是我这些年被喷出来的血泪经验,全程大白话,代码直接复制粘贴就能用。看完不敢说让你变成架构师,但至少下次写接口的时候,后端同事不会再给你发那个"无语"的表情包了。

RESTful到底是个啥玩意儿,别被名字吓到

RESTful这个词听起来贼高级,全称是什么Representational State Transfer,翻译过来叫"表现层状态转化"。我第一次看到这翻译的时候,脑子里就一句话:说人话会死吗?

其实吧,你可以把它理解成一套"讲礼貌"的接口设计礼仪。它的核心思想就一句话:把网络上的所有东西都当成"资源",然后用HTTP协议自带的那些方法(GET、POST、PUT、DELETE)去操作这些资源。

举个接地气的例子。想象你开了一家中餐馆,RESTful就是那张菜单上的规矩:

  • 顾客点餐(GET):“老板,给我看看你们家有啥菜”——这是获取资源
  • 顾客下单(POST):“我要一份宫保鸡丁”——这是新建资源
  • 顾客换菜(PUT/PATCH):“刚才那个不要辣,换成微辣”——这是修改资源
  • 顾客退单(DELETE):“这菜太难吃了,撤了”——这是删除资源

看到没?全程没有出现"do"、“make”、"get"这种动词,但你一看就明白是在干嘛。这就是RESTful的精髓——用名词表示资源,用HTTP方法表示动作

很多人误以为RESTful是一种技术或者框架,其实它就是一种设计风格。就像你写CSS可以用BEM命名规范一样,用不用随你,但用了代码看起来更专业。而且说实话,现在市面上稍微有点规模的公司,接口设计基本都是按这个套路来的,你不会这个,跳槽都不好意思跟HR聊。

核心原则掰开揉碎讲,这些规矩真不是后端装高冷

好了,既然知道RESTful是个啥了,那咱们就聊聊具体怎么落地。我总结了几条保命原则,你照着做,至少不会闹笑话。

资源必须用名词,动词滚出URL

这是最容易踩的坑。我见过太多新手写/getUsers/createOrder/deleteProduct这种URL了。说实话,功能上没毛病,但看起来就像把英语作业直接贴在了地址栏里。

正确的姿势是:

// 错误示范,满满的野生程序猿味道
GET /getUserInfo?id=123
POST /createNewOrder
DELETE /deleteProduct/456

// 正确姿势,看起来就很专业
GET /users/123          // 获取ID为123的用户
POST /orders            // 创建新订单
DELETE /products/456    // 删除ID为456的商品

看到区别了吗?URL里只有名词(users、orders、products),动作全靠HTTP方法来表达。GET就是获取,POST就是创建,PUT就是全量更新,PATCH就是局部更新,DELETE就是删除。这是HTTP协议本来就有的语义,你非得在URL里再写一遍,就像你在微信里发消息:“我在吃饭(eating)”,括号里的英文纯属多余,还显得挺呆。

复数形式是政治正确

关于用单数还是复数,社区里其实争论了好多年。有人觉得/user/123/users/123更直观,因为返回的是单个对象。但主流做法是用复数,为啥呢?因为资源集合本身就是复数概念。

// 获取所有用户
GET /users

// 获取单个用户
GET /users/123

// 获取某个用户的所有订单
GET /users/123/orders

// 获取某个用户的某个具体订单
GET /users/123/orders/456

你看,用复数的话,层级关系特别清晰。/users是用户池,/users/123是池子里的某一条鱼,/users/123/orders是这条鱼的所有订单。如果用单数/user/123/orders,读起来就有点别扭,好像这个user下面挂了个orders,语法上怪怪的。

当然,如果你就是喜欢用单数,也行,关键是整个项目要统一。别这边用/users,那边又用/order,那前后端不打起来才怪。

层级别嵌套太深,不然URL能绕地球三圈

RESTful推荐用层级关系表达关联资源,比如/users/123/orders/456,一眼就能看出这是用户123的456号订单。但千万别嵌套太深,我见过最离谱的接口长这样:

// 离谱他妈给离谱开门,离谱到家了
GET /shops/1/departments/2/categories/3/products/4/reviews/5/comments/6/authors/7

这谁受得了?URL长得跟绕口令似的。一般来说,超过三层嵌套就该考虑换个设计了。比如上面那个,完全可以拆成:

// 先拿到评论ID,再查作者
GET /reviews/5
// 返回里带上authorId,然后
GET /users/7

或者直接用查询参数:

GET /comments?reviewId=5&authorId=7

记住,URL是给人看的,不是给机器压缩的,别为了省那么一两次请求就把路径搞成俄罗斯套娃。

URL怎么起名才不被嘲笑

起名字这事儿,说简单也简单,说难也难。我见过有团队为了/userOrders还是/user-orders还是/user_orders吵了整整一个下午,最后CTO拍板用/user_orders,结果前端小哥默默吐槽:“这看起来像个Python变量名…”

大小写与分隔符的江湖恩怨

关于命名规范,业内主要有三派:

驼峰派/userOrders/orderItems

  • 优点:Java程序员看着亲切
  • 缺点:URL本身就是大小写不敏感的(虽然规范说敏感,但很多服务器配置会搞成不敏感),而且看起来有点挤

中划线派/user-orders/order-items

  • 优点:可读性好,谷歌推荐,SEO友好
  • 缺点:Shift键按得手指疼

下划线派/user_orders/order_items

  • 优点:Python/PHP程序员看着亲切,不用按Shift
  • 缺点:URL里下划线有时候会被识别成空格,而且看着像数据库表名

我的建议是用中划线(kebab-case),因为RFC 3986标准里明确说中划线是"unreserved character",不会引起歧义。而且你看GitHub的API,全是/repos/{owner}/{repo}/issues这种,清爽得很。

// 推荐这样写
GET /user-orders/123
GET /product-categories/electronics

// 别这样写  
GET /userOrders/123        // 驼峰在URL里看着累
GET /user_orders/123       // 下划线像蛇精病
GET /userorders/123        // 这谁看得懂啊

版本号放哪儿这是个哲学问题

API肯定要迭代,今天v1明天v2的。版本号放哪儿呢?主要有三种流派:

Path派/v1/users/v2/users

// 优点:直观,一眼就知道是哪个版本
// 缺点:URL里多个前缀,有点丑
app.get('/v1/users', (req, res) => { ... });
app.get('/v2/users', (req, res) => { ... });

Header派:在Header里加API-Version: v1

// 优点:URL干净
// 缺点:测试的时候麻烦,curl命令要多写个header,浏览器直接访问也看不到版本

Content-Type派Accept: application/vnd.api.v1+json

// 优点:最RESTful,最专业的写法
// 缺点:太专业了,一般团队hold不住,前端看了想打人

我的建议:小团队用Path派,简单粗暴;大团队用Header派,显得高级;Content-Type派除非你们公司有个架构师闲着没事干,否则别折腾。

而且版本号用v1、v2就行,别用什么1.0、1.1这种,看着像软件版本号,容易和API版本搞混。

过滤、排序、分页别往URL里塞动词

查询资源的时候,肯定要带条件。这时候别傻乎乎地写/getActiveUsers或者/searchUsers用Query参数

// 错误示范,动词又出现了
GET /getActiveUsers
GET /searchUsersByName?name=张三

// 正确姿势,Query参数走起来
GET /users?status=active
GET /users?name=张三&age=25
GET /users?sort=-createdAt&page=2&limit=20  // -表示降序

看到没?基础路径永远是/users,各种条件用问号挂后面。这样既符合RESTful风格,又灵活,加条件不用改路由。

分页参数建议用pagelimit(或者size),offset那套虽然专业,但前端同事一般都习惯page:

// 推荐
GET /users?page=1&limit=20

// 或者
GET /users?page=1&size=20

// offset也不是不行,但得跟前端对齐
GET /users?offset=0&limit=20

HTTP方法别乱用,GET不是万能的

我敢说,80%的接口混乱都是因为HTTP方法用错了。很多人不管干啥都是POST,获取数据POST,提交数据POST,删除数据还是POST,问就是"POST稳一点"。

稳个屁啊!HTTP方法本身就是一种语义表达,你用错了,状态码就不知道该咋给,缓存策略也会出问题。

GET:只读,且无副作用

GET方法就是用来获取资源的,而且必须是幂等的(幂等就是调一万次结果都一样)和安全的(安全就是不改服务器数据)。

// 正确的GET用法
GET /users/123          // 获取用户123的信息
GET /users?page=2       // 获取第二页用户列表
GET /products?category=phone&sort=price  // 按条件筛选并排序

// 错误的GET用法(虽然能跑,但被人看到会笑话)
GET /users/123/delete   // 删除用户?用GET删除数据,胆子真大
GET /users/create?name=张三  // 创建用户?GET请求有长度限制,数据大了就GG

GET请求的几个忌讳:

  1. 别在GET里传敏感信息:因为URL会留在浏览器历史、服务器日志里,你传个密码试试?
  2. 别用GET做数据修改:有些浏览器或者爬虫会预加载GET请求,你写个GET /transfer-money?to=黑客&amount=10000,人家爬虫一遍历,钱就没了。
  3. 注意URL长度限制:不同浏览器不一样,一般2KB到8KB,数据多了用POST。

POST:专职创建资源

POST就是用来创建新资源的,而且在RESTful里,POST的URL是复数资源路径,因为你是往这个集合里加新成员。

// 创建新用户
POST /users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "password": "123456"  // 别真这么干,要加密
}

// 返回201 Created,Location头指向新资源
HTTP/1.1 201 Created
Location: /users/123

{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com",
  "createdAt": "2024-01-15T10:30:00Z"
}

看到没?POST之后返回201状态码,表示"创建成功",并且在Header里给个Location,告诉前端"新资源在这儿呢"。这叫懂礼貌,前端拿到Location可以直接跳转到详情页,省得再组装URL。

POST不是垃圾桶,别啥都往里塞。有的同学更新数据也用POST,理由是"POST参数多",其实PUT/PATCH也能带body,而且语义更清晰。

PUT vs PATCH:全量更新和打补丁的区别

这俩是最容易搞混的。简单说:

  • PUT:全量更新,你把整个资源的新版本发过去,没有的字段就置空
  • PATCH:局部更新,你只发要改的字段,其他字段保持不变
// 用户原数据
{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 25,
  "avatar": "http://xxx.jpg"
}

// PUT请求(全量更新)
PUT /users/123
{
  "name": "张三丰",  // 改名了
  "email": "zhangsan@example.com",  // 没改也得发
  "age": 25,  // 没改也得发
  "avatar": "http://xxx.jpg"  // 没改也得发
}

// 如果PUT的时候漏了字段,比如只发了个name
PUT /users/123
{
  "name": "张三丰"
}
// 结果:email、age、avatar全被清空或设为null,因为PUT是"替换"整个资源
// PATCH请求(局部更新)
PATCH /users/123
{
  "name": "张三丰"  // 只发要改的字段
}

// 结果:只有name变了,email、age、avatar保持原样

实际项目中,PATCH更实用,因为前端往往只改一两个字段,没必要把整个对象都发过去。但PUT也有用,比如"保存草稿"功能,用户写一半刷新页面,你PUT上去,下次GET下来还是完整的一份。

DELETE:删数据真得用,别怕

很多人不敢用DELETE方法,觉得不吉利,或者怕搜索引擎误删数据(其实搜索引擎不会用DELETE)。结果全用POST /users/123/delete,看着就别扭。

// 正确的删除
DELETE /users/123

// 返回204 No Content(删除成功,没东西可返回)
HTTP/1.1 204 No Content

// 或者返回200,带上被删的数据(防止误删后悔)
HTTP/1.1 200 OK
{
  "message": "用户已删除",
  "deletedUser": {
    "id": 123,
    "name": "张三"
  }
}

注意:DELETE也是幂等的,删一次和删十次效果一样(都是没了)。但别写成像DELETE /users这种批量删除,除非你真的想删库跑路。要批量删,用Query参数:

// 批量删除指定ID的用户
DELETE /users?id=1,2,3,4,5

// 或者(更RESTful一点)
POST /users/batch-delete  // 如果DELETE带body有争议的话
{
  "ids": [1, 2, 3, 4, 5]
}

状态码别只会200和500,数字背后都是暗号

我见过最离谱的接口,不管成功失败全返回200,然后在body里自己定义一套code。前端调接口,HTTP状态是200,body里{code: -1, msg: "登录失败"},前端的路由拦截器一看200,以为成功了,结果页面跳转到登录后的首页,然后因为没数据又报错,用户体验稀碎。

2xx:成功系列,但不只是200

  • 200 OK:通用成功,GET、PUT、PATCH、DELETE成功都可以用
  • 201 Created:POST创建资源成功,必须配合Location头使用
  • 204 No Content:删除成功,或者更新成功但没啥可返回的(省流量)
  • 202 Accepted:已接受请求,但还没处理完(比如异步任务,“你的导出Excel请求已提交,稍后邮件发送”)
// 创建成功返回201
app.post('/orders', (req, res) => {
  const newOrder = createOrder(req.body);
  res.status(201)
     .location(`/orders/${newOrder.id}`)
     .json(newOrder);
});

// 删除成功返回204
app.delete('/users/:id', (req, res) => {
  deleteUser(req.params.id);
  res.status(204).send();  // 204不能带body,或者带空对象
});

4xx:客户端错了,别甩锅给服务器

4xx系列表示客户端请求有问题,不是服务器崩了,是前端传参传错了,或者用户没权限。

  • 400 Bad Request:请求格式错了,比如JSON格式不对,或者必填字段没填
  • 401 Unauthorized:未认证,用户没登录(虽然叫 unauthorized,但实际上是未认证)
  • 403 Forbidden:已认证但没权限,比如普通用户想访问管理员接口(这个叫授权失败)
  • 404 Not Found:资源不存在,比如/users/999,但ID为999的用户不存在
  • 409 Conflict:资源冲突,比如注册用户时邮箱已存在,或者并发修改冲突
  • 422 Unprocessable Entity:语义错误,比如要求填手机号,前端传了个"abc"(虽然格式是JSON,但内容验证失败)
  • 429 Too Many Requests:请求太频繁,被限流了,前端应该稍后再试
// 400示例:参数格式错误
app.post('/users', (req, res) => {
  if (!req.body.email || !req.body.email.includes('@')) {
    return res.status(400).json({
      error: 'Bad Request',
      message: '邮箱格式不正确',
      field: 'email'
    });
  }
  // ...
});

// 401 vs 403的区别,这个面试常考
app.get('/admin/dashboard', authenticate, (req, res) => {
  if (!req.user) {
    return res.status(401).json({ error: '请先登录' });  // 没登录
  }
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: '权限不足' });  // 登录了但不是管理员
  }
  res.json({ data: '敏感数据' });
});

// 409:资源冲突
app.post('/register', (req, res) => {
  if (userExists(req.body.email)) {
    return res.status(409).json({
      error: 'Conflict',
      message: '该邮箱已被注册'
    });
  }
});

重要提示:4xx错误返回时,一定要在body里告诉前端具体哪错了。别只返回个"Bad Request",前端看了想打人。要告诉他是哪个字段错了,为什么错。

5xx:服务器凉了,赶紧找后端

5xx是服务器内部错误,前端看到5xx基本无能为力,只能给用户显示"系统繁忙,请稍后再试"。

  • 500 Internal Server Error:通用服务器错误,代码抛异常了
  • 502 Bad Gateway:网关挂了,一般是Nginx配置问题或后端服务没启动
  • 503 Service Unavailable:服务暂时不可用,比如服务器维护中或过载
  • 504 Gateway Timeout:网关超时,后端处理太慢了
// 全局错误处理,别让前端看到堆栈信息
app.use((err, req, res, next) => {
  console.error(err.stack);  // 自己记下来
  
  // 生产环境别暴露错误细节,防止泄露敏感信息
  res.status(500).json({
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'production' 
      ? '服务器内部错误' 
      : err.message,
    requestId: req.id  // 给前端一个请求ID,方便后端查日志
  });
});

实际项目里怎么玩转RESTful,真实场景设计

光讲理论没意思,咱们来几个实战案例,看看用户系统、电商订单、社交评论这些常见功能怎么设计接口。

用户管理模块

这是最基础的,但坑也多。

// 用户注册
POST /auth/register
{
  "email": "user@example.com",
  "password": "securePassword123",
  "nickname": "前端小王"
}
// 返回201,注意别返回密码,哪怕加了密

// 用户登录(这个其实不算RESTful资源,用POST也合理)
POST /auth/login
{
  "email": "user@example.com",
  "password": "securePassword123"
}
// 返回200 + JWT Token,或者返回204 + Set-Cookie头

// 获取当前用户信息
GET /users/me  // 或者用 /auth/profile,看团队约定
Authorization: Bearer <token>

// 获取用户列表(带过滤和分页)
GET /users?page=1&limit=20&role=admin&sort=-createdAt
Authorization: Bearer <token>  // 只有管理员能看列表

// 获取指定用户详情
GET /users/123
// 注意:用户只能看自己的详情,或者管理员能看所有人的,这里要做权限控制

// 更新用户信息
PATCH /users/123  // 用户自己改资料
{
  "nickname": "前端老王",
  "avatar": "http://new-avatar.jpg"
}

// 修改密码(单独接口,因为要验证旧密码)
PUT /users/123/password  // 或者 PATCH /users/123
{
  "oldPassword": "securePassword123",
  "newPassword": "newSecurePassword456"
}

// 注销账号(软删除,别真删数据)
DELETE /users/123
// 或者 POST /users/123/deactivate 如果公司规定不让用DELETE

注意点

  • 密码相关操作要单独接口,别和更新用户信息混在一起,安全要求不一样
  • 别在GET /users里返回密码字段,哪怕是加密的,这是大忌
  • 软删除:很多时候别真DELETE,而是加个isActive字段,删的时候改成false,防止误删后悔

电商订单系统

订单系统稍微复杂点,涉及状态流转和库存。

// 创建订单(从购物车结算)
POST /orders
{
  "items": [
    {"productId": "P001", "quantity": 2, "skuId": "SKU-RED-L"},
    {"productId": "P002", "quantity": 1, "skuId": "SKU-BLUE-M"}
  ],
  "addressId": "ADDR-123",
  "couponCode": "SAVE20"
}
// 返回201,Location: /orders/ORDER-20240115-001
// 注意:创建订单时要扣库存,这是个事务操作

// 获取订单列表(用户只能看自己的)
GET /orders?status=paid&page=1&limit=10
// 返回当前登录用户的订单,管理员可以看 ?userId=xxx 查指定用户的

// 获取订单详情
GET /orders/ORDER-20240115-001
// 返回订单完整信息,包括商品快照、物流信息、支付状态等

// 取消订单(不是DELETE,因为订单数据要保留)
PATCH /orders/ORDER-20240115-001
{
  "status": "cancelled",
  "reason": "不想买了"
}
// 或者专门的动作接口
POST /orders/ORDER-20240115-001/cancel  // 如果动作很复杂的话
{
  "reason": "不想买了"
}

// 支付订单(这通常调用支付网关,但可以先创建支付单)
POST /orders/ORDER-20240115-001/payments
{
  "paymentMethod": "alipay",
  "returnUrl": "http://your-site.com/payment/callback"
}
// 返回支付页面URL或支付参数

// 申请退款
POST /orders/ORDER-20240115-001/refunds
{
  "reason": "商品质量问题",
  "items": [{"productId": "P001", "quantity": 1}],  // 部分退款
  "images": ["http://evidence1.jpg", "http://evidence2.jpg"]
}

设计思考

  • 订单状态变更用PATCH还是POST? 如果状态变更有复杂业务逻辑(比如取消订单要还原库存、发通知、退优惠券),建议用POST /orders/{id}/cancel;如果只是简单改个字段,用PATCH就行。
  • 订单ID怎么生成? 别用自增ID,容易暴露业务量。用ORDER-年月日-随机数这种格式,或者雪花算法。

评论和点赞系统

社交功能,要考虑并发和防刷。

// 获取文章评论列表(树形结构)
GET /articles/123/comments?page=1&limit=20&sort=-createdAt
// 返回评论数组,每个评论包含:
// - 评论内容、作者信息
// - 点赞数、是否已点赞(根据当前登录用户)
// - 回复数、前几条回复预览

// 发表评论(一级评论)
POST /articles/123/comments
{
  "content": "写得真好!",
  "parentId": null  // 一级评论parentId为null
}

// 回复评论(二级评论)
POST /articles/123/comments
{
  "content": "谢谢支持!",
  "parentId": "COMMENT-456"  // 回复哪条评论
}

// 或者更清晰的层级
POST /comments/COMMENT-456/replies
{
  "content": "谢谢支持!"
}

// 点赞评论(幂等操作,点两次是取消点赞)
PUT /comments/COMMENT-456/like
// 或者 POST /comments/COMMENT-456/likes,看怎么设计
// 返回204或更新后的点赞数

// 删除评论(软删除,显示"该评论已删除")
DELETE /comments/COMMENT-456
// 或者 PATCH /comments/COMMENT-456 { "isDeleted": true }

注意

  • 点赞这种操作要防重复,数据库里用唯一索引(userId, targetId, targetType)
  • 评论列表要考虑性能,如果一篇文章有几万条评论,别一次性返回,要分页,而且深度嵌套的回复要考虑用"展开更多"的方式

踩过的坑比写过的代码还多

这些年我踩的坑,能填满一个太平洋。这里挑几个没人告诉你但一踩就炸的坑说说。

分页参数到底叫page还是offset?

后端喜欢offset,因为数据库查询用LIMIT 20 OFFSET 40这种SQL很直观。前端喜欢page,因为UI上显示"第2页"比"从第40条开始"更友好。

建议:支持两种!或者至少和前端对齐。如果只做一种,推荐page+limit,因为offset有个坑:如果在用户翻页的过程中,有新数据插入,offset会漂移,导致用户可能看到重复数据或跳过数据。

// 方案1:传统分页(可能有漂移问题)
GET /users?page=2&limit=20  // 后端转成 OFFSET 20 LIMIT 20

// 方案2:游标分页(适合实时性强的列表,比如微博朋友圈)
GET /users?cursor=eyJpZCI6MTIzfQ==&limit=20  
// cursor是上一页最后一条数据的ID加密后的值,数据库用 WHERE id > cursor LIMIT 20
// 这样即使有新数据插入,也不会影响当前查询

过滤条件塞query还是body?

GET请求理论上可以带body,但很多服务器和库不支持(比如axios默认会把GET的body忽略掉)。所以过滤条件简单(几个字段)用Query,复杂(嵌套对象、数组)用POST

// 简单过滤,Query搞定
GET /products?category=phone&priceMin=1000&priceMax=5000

// 复杂过滤,比如电商的"多选属性筛选"
// 这种如果硬塞Query会变成 ?attrs=color:red,blue;size:M,L,解析起来很痛苦
POST /products/search  // 妥协一下,用POST做查询
{
  "category": "phone",
  "priceRange": [1000, 5000],
  "attributes": {
    "color": ["red", "blue"],
    "size": ["M", "L"]
  },
  "sort": [{"field": "price", "order": "asc"}],
  "page": 1,
  "limit": 20
}

注意:虽然RESTful purist会说"查询用GET",但实用主义优先,复杂查询用POST是业内常见做法,别纠结。

文件上传算不算RESTful?

严格来说,文件上传(multipart/form-data)不太符合RESTful的"资源"概念,因为传的不是JSON。但生意还得做啊!

// 方案1:单独接口(推荐)
POST /uploads  // 或者 /files
Content-Type: multipart/form-data

// 返回文件URL或文件ID
{
  "fileId": "FILE-123",
  "url": "http://cdn.example.com/files/123.jpg",
  "size": 102456,
  "mimeType": "image/jpeg"
}

// 然后业务接口只传fileId
POST /users/123/avatar
{
  "fileId": "FILE-123"
}

// 方案2:Base64编码塞JSON(不推荐,文件大了性能差)
POST /users/123/avatar
{
  "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..." 
}

大文件上传要用分片,别直接传几百MB的文件,容易断。可以用OSS直传(前端直传阿里云/七牛云,后端只拿URL),或者用 tus 协议做断点续传。

批量操作怎么设计?

RESTful对批量操作支持不太好,因为URL和HTTP方法都是针对单个资源的。

// 方案1:逗号分隔ID(简单场景)
GET /users?id=1,2,3,4,5  // 获取多个用户
DELETE /users?id=1,2,3   // 批量删除(有些服务器不支持DELETE带Query,慎用)

// 方案2:批量接口(复杂场景)
POST /users/batch-delete
{
  "ids": [1, 2, 3, 4, 5]
}

POST /users/batch-update
{
  "ids": [1, 2, 3],
  "data": {
    "status": "active"
  }
}

// 方案3:GraphQL(如果批量操作特别多,考虑上GraphQL)

关联资源是嵌套还是扁平?

获取订单详情时,要不要把用户信息嵌套进去?

// 方案1:扁平,只返回ID,前端再查(请求次数多,但缓存友好)
GET /orders/123
{
  "id": "ORDER-123",
  "userId": "USER-456",
  "totalAmount": 199.9
}
// 前端再 GET /users/USER-456

// 方案2:嵌套,一次返回(请求次数少,但数据可能重复)
GET /orders/123
{
  "id": "ORDER-123",
  "user": {
    "id": "USER-456",
    "name": "张三",
    "avatar": "http://xxx.jpg"
  }
}

// 方案3:可选嵌入(用Query控制)
GET /orders/123?embed=user,items  // 嵌入用户和订单项
GET /orders/123?fields=id,totalAmount,user.name  // 只返回特定字段(GraphQL-lite)

建议:看场景。如果移动端(网络不稳定,请求次数越少越好),用嵌套;如果Web端(HTTP/2多路复用,请求开销小),用扁平,配合缓存更灵活。

调试和排查接口问题的野路子

接口写完了,得测啊。别跟我说你直接用浏览器地址栏测GET请求,那也太原始了。

Postman高级玩法

别只会点Send,这几个功能用起来:

// 1. 环境变量,切换开发和生产环境
// 在Postman右上角设置Environment,定义 {{baseUrl}}、{{token}}

// 2. 预请求脚本(自动登录)
// 在Collection的Pre-request Scripts里写:
pm.sendRequest({
  url: "{{baseUrl}}/auth/login",
  method: "POST",
  body: {
    mode: "raw",
    raw: JSON.stringify({email: "test@test.com", password: "123456"})
  }
}, function (err, res) {
  const jsonData = res.json();
  pm.environment.set("token", jsonData.token);
});

// 3. 测试脚本(自动校验响应)
// 在Tests标签页写:
pm.test("Status code is 200", function () {
  pm.response.to.have.status(200);
});
pm.test("Response has user id", function () {
  var jsonData = pm.response.json();
  pm.expect(jsonData).to.have.property("id");
});

浏览器Network面板看门道

打开F12,别只看Console,Network才是宝藏:

  1. 看Timing:DNS查询、SSL握手、TTFB(首字节时间)各花了多久。如果TTFB太长,说明后端处理慢;如果SSL握手长,考虑开启TLS 1.3。
  2. 看Headers:重点看Request Headers里的AcceptContent-Type,Response Headers里的Cache-ControlETag
  3. 看Preview和Response的区别:Preview是格式化后的,Response是原始文本。如果Preview显示乱码但Response正常,可能是Content-Type设置错了。

后端返回400但不说哪错了咋办?

这是最抓狂的情况。你得自己加日志

// 前端拦截器里,遇到4xx错误把请求详情打出来
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response && error.response.status >= 400 && error.response.status < 500) {
      console.group(`🚨 API Error ${error.response.status}`);
      console.log('Request URL:', error.config.url);
      console.log('Request Method:', error.config.method);
      console.log('Request Headers:', error.config.headers);
      console.log('Request Data:', error.config.data);
      console.log('Response Data:', error.response.data);
      console.groupEnd();
      
      // 还可以上报到Sentry等错误监控系统
    }
    return Promise.reject(error);
  }
);

然后拿curl命令直接在后端环境跑,排除前端问题:

# 在Network面板里右键请求 -> Copy -> Copy as cURL
# 粘贴到终端,看是不是还报错。如果curl也报错,那就是后端问题
curl -X POST "http://api.example.com/users" \
  -H "Content-Type: application/json" \
  -d '{"name":"test"}'

线上问题排查,日志里加Request ID

微服务架构下,一个请求可能经过A->B->C三个服务,出问题的时候得串联日志。

// 前端生成唯一请求ID(或者后端网关生成)
const requestId = generateUUID();

// 每个请求都带这个ID
axios.defaults.headers.common['X-Request-ID'] = requestId;

// 后端把这个ID打印到所有日志里,排查的时候按ID搜索
// 后端日志:[2024-01-15 10:30:00] [REQ-abc-123] [UserService] 开始查询用户
//          [2024-01-15 10:30:01] [REQ-abc-123] [OrderService] 开始查询订单

让接口更香的小技巧

基础功能实现了,还能怎么优化?这几个技巧用上,前端同事会感谢你。

统一响应格式

别这个接口返回{code: 0, data: {...}},那个接口返回{success: true, result: {...}},前端封装拦截器的时候想骂人。

// 统一结构
{
  "status": "success",  // 或 "error"
  "data": { ... },      // 成功时的数据
  "error": {            // 失败时的详情
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "details": { ... }   // 可选,详细错误信息
  },
  "meta": {             // 元数据(分页等)
    "page": 1,
    "limit": 20,
    "total": 100,
    "timestamp": "2024-01-15T10:30:00Z",
    "requestId": "req-abc-123"
  }
}

// 或者简单点,HTTP状态码已经表达成功失败了,body里只放数据
// 成功:
HTTP/1.1 200 OK
{
  "id": 123,
  "name": "张三",
  "meta": {
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

// 失败:
HTTP/1.1 404 Not Found
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "用户ID 123 不存在",
    "resourceType": "User",
    "resourceId": "123"
  }
}

错误结构标准化

前端要根据错误码做不同处理,比如INVALID_TOKEN要跳登录页,INSUFFICIENT_BALANCE要提示充值。

// 定义错误码常量
const ErrorCodes = {
  // 认证授权类 1xxx
  INVALID_TOKEN: { code: 1001, status: 401, message: '登录已过期' },
  INSUFFICIENT_PERMISSIONS: { code: 1002, status: 403, message: '权限不足' },
  
  // 业务逻辑类 2xxx
  USER_NOT_FOUND: { code: 2001, status: 404, message: '用户不存在' },
  ORDER_EXPIRED: { code: 2002, status: 400, message: '订单已过期' },
  INSUFFICIENT_BALANCE: { code: 2003, status: 400, message: '余额不足' },
  
  // 系统错误类 5xxx
  INTERNAL_ERROR: { code: 5000, status: 500, message: '系统繁忙' },
  DATABASE_ERROR: { code: 5001, status: 500, message: '数据库错误' }
};

// 使用
if (user.balance < order.amount) {
  throw new ApiError(ErrorCodes.INSUFFICIENT_BALANCE, {
    currentBalance: user.balance,
    requiredAmount: order.amount
  });
}

HATEOAS做超媒体导航(装X必备)

HATEOAS(Hypermedia as the Engine of Application State)听起来高大上,其实就是在返回数据里带上相关操作的链接。

// 普通返回
GET /orders/123
{
  "id": "123",
  "status": "pending",
  "total": 199.9
}

// HATEOAS风格
GET /orders/123
{
  "id": "123",
  "status": "pending",
  "total": 199.9,
  "_links": {
    "self": { "href": "/orders/123" },
    "user": { "href": "/users/456" },
    "items": { "href": "/orders/123/items" },
    "pay": { "href": "/orders/123/payments", "method": "POST" },  // 可以执行的操作
    "cancel": { "href": "/orders/123", "method": "DELETE" }
  }
}

优点:前端不需要硬编码URL,完全根据后端返回的_links决定展示哪些按钮。缺点:JSON体积变大,而且前端逻辑变复杂(得解析_links)。一般只有特别追求RESTful纯度的项目会用,比如Spring Data REST默认就带这个。

文档自动生成真香警告

别手写Word文档了,用工具自动生成:

  • Swagger/OpenAPI:写代码的时候加注释,自动生成在线文档,还能直接在线测试
  • Postman Collection:导出JSON文件,前端导入就能用
  • API Blueprint:Markdown语法写API文档,适合技术写作
// Swagger示例(使用swagger-jsdoc)
/**
 * @swagger
 * /users:
 *   get:
 *     summary: 获取用户列表
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: 页码
 *     responses:
 *       200:
 *         description: 成功
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/User'
 */
app.get('/users', (req, res) => { ... });

别以为写完就完了,前后端联调的潜规则

接口设计好了,代码写完了,以为可以松口气了?Too young!前后端联调才是真正的战场

前端怎么优雅处理加载态和错误提示

别每个请求都写try-catch,封装一个useApi钩子:

// React示例
function useApi(apiFunction) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const execute = async (...params) => {
    setLoading(true);
    setError(null);
    try {
      const result = await apiFunction(...params);
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      // 统一错误处理
      if (err.response?.status === 401) {
        message.error('登录已过期,请重新登录');
        router.push('/login');
      } else {
        message.error(err.response?.data?.message || '请求失败');
      }
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { data, loading, error, execute };
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error, execute: fetchUser } = useApi(getUser);
  
  useEffect(() => {
    fetchUser(userId);
  }, [userId]);

  if (loading) return <Skeleton active />;
  if (error) return <ErrorDisplay error={error} onRetry={() => fetchUser(userId)} />;
  
  return <div>{user.name}</div>;
}

Mock数据怎么跟RESTful规则对齐

前端开发的时候后端接口可能还没ready,这时候Mock数据要遵循同样的规范,别到时候对接的时候一地鸡毛。

// 使用MSW (Mock Service Worker)
import { rest } from 'msw';

export const handlers = [
  // Mock GET /users
  rest.get('/api/users', (req, res, ctx) => {
    const page = req.url.searchParams.get('page') || '1';
    
    return res(
      ctx.status(200),
      ctx.json({
        data: [
          { id: 1, name: 'Mock用户1' },
          { id: 2, name: 'Mock用户2' }
        ],
        meta: {
          page: parseInt(page),
          total: 100
        }
      })
    );
  }),

  // Mock POST /users,返回201和Location头
  rest.post('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(201),
      ctx.set('Location', '/api/users/999'),
      ctx.json({
        id: 999,
        ...req.body,
        createdAt: new Date().toISOString()
      })
    );
  })
];

上线后改接口字段怎么不背锅

最怕的就是上线后改接口,前端没跟着改,直接报错。几个保命技巧:

  1. 版本控制:/v1/xxx 和 /v2/xxx并存,老客户端继续调v1,新功能用v2
  2. 字段兼容性:别删老字段,先标记deprecated,过几个版本再删
  3. 灰度发布:先给内部用户或1%流量用新接口,观察没问题再全量
  4. 契约测试:用Pact之类的工具,前后端约定好契约,谁违反谁Pipeline失败
// 字段废弃标记
{
  "id": 123,
  "name": "张三",
  "nickname": "法外狂徒",  // 新字段
  "displayName": "法外狂徒", // 老字段,已废弃,但为了兼容还保留,value和nickname一样
  "_deprecated": {
    "displayName": "请使用nickname字段,displayName将在v2.0移除"
  }
}

写到这儿差不多也该收尾了。说真的,RESTful这玩意儿理解起来不难,难的是坚持执行。我见过太多团队,刚开始信誓旦旦说我们要做RESTful API,写到后面就变成"能用就行",最后文档和代码对不上,前后端天天吵架。

我的建议是:不要追求100%纯RESTful,有些场景(比如批量操作、复杂查询)该妥协就妥协,但基础的URL命名、HTTP方法、状态码这些底线要守住。至少做到让前端看到你的接口,不用看文档就能猜出怎么调,这就成功了。

最后送大家一句话:好的API设计就像好的笑话——不需要解释。如果你写了个接口,得跟前端解释半天为啥URL长这样、为啥返回500但实际是前端参数错了,那这接口就该回炉重造了。

好了,工友们,去写接口吧,记得别再写/getUserInfo了,我看着害怕。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值