“某小公司 RESTful、共用接口、前后端分离、接口约定的实践”,光看这个标题我就有点感慨。十年前前后端分离还是个“大厂专属”的架构名词,现在连三五个人、甚至两三个人的小团队都在搞。但小公司搞前后端分离,跟大厂完全是两码事:没有专职架构师,没有完善的基建,更没有充足的排期让你慢慢磨规范。我在这种体量的团队里做过几轮从零到一的接口体系搭建,踩过的坑比写过的接口还多,这里把整个过程摊开聊一聊。
这套实践解决的核心问题其实很朴素:怎么让一个后端、两三个前端、一个测试的小团队,在不用花太多管理成本的前提下,把接口定义清楚、把联调冲突降到最低、把返工率打下来。适合正在做前后端分离但总觉得“哪里不对劲、又说不上来”的中小型技术团队参考,也适合刚接手一个半成品接口体系的同学作为自查清单。
1. 项目背景与整体设计思路
1.1 小公司做前后端分离的真实动机
我见过不少小团队是被“大厂都这么干”推着走,稀里糊涂就把前后端拆了。人还是那几个人,流程还是那套流程,只是把原本后端渲染的页面换成了前端框架的页面,结果效率不但没提升,反而因为联调成本暴增,三天两头互相扯皮。
真正适合小公司做前后端分离的动机,其实只有三个:
- 业务确实需要多端复用,同一套接口要撑起管理后台、用户端网页甚至未来的小程序
- 前后端排期严重不对等,前端要提前开发,后端又不想用模板硬套
- 后端不想再被页面交互细节绑架,想专注业务逻辑本身
如果不是这三个原因,老老实实做服务端渲染反而更省事。我们当时就是第一个原因占了主导——老板一句话“后面可能要出小程序”,才下决心把所有接口全部RESTful化。
1.2 方案选型:为什么没有直接上微服务
小团队最容易犯的错是照搬大厂技术栈。我就见过5个人的后端团队硬上微服务,光注册中心、配置中心、网关就折腾了两个月,业务一行没写。我们当时的判断是:业务体量根本撑不起微服务的复杂度,与其拆服务,不如把单体的接口边界划清楚。
选型的核心权衡点在于:
- 单体应用 + 模块化分包:后端一个工程,按业务模块分包,接口层统一收口
- RESTful风格:不用RPC、不用GraphQL,理由很简单——学习成本低、前端同学天然熟悉、调试工具链成熟
- 共用接口:以角色区分接口权限,而不是按端拆接口,减少重复逻辑
这套组合对整个团队的技术栈要求不高,后端会Spring Boot(我们用的是这个,其他语言类似框架同理),前端会Vue或React,就能跑起来。若依这类开源框架的思路也是这么干的,但我们没有直接拿来用,因为开源框架自带的权限模型和业务耦合度太高,改起来比重写还累。
1.3 接口约定的整体框架
接口约定不是靠一份文档就完事的,它需要形成一套能在日常开发中自然执行的规则。我们的设计分三层:
| 层次 | 内容 | 负责角色 |
|---|---|---|
| 约定层 | 命名规范、状态码、错误码、分页格式、时间格式 | 后端主导,全员确认 |
| 技术层 | RESTful风格、共用接口模型、权限校验框架 | 后端实现 |
| 协作层 | 接口文档、Mock数据、联调流程、变更通知 | 全员参与 |
这个框架看起来很普通,但真正难的是让所有人都“认账”。我们用了最土的办法——每次接口评审全员到场,白板上把接口一个个过,确认没问题才写代码。宁可评审多花一小时,也不要接口返工花一天。
2. RESTful落到细节:那些教科书上没写的取舍
2.1 资源命名:用名词,但别被名词绑架
RESTful的核心是用资源视角建模接口。比如用户、订单、商品,对应/api/users、/api/orders、/api/products,这个所有人都能接受。但一遇到“某个操作不是典型的增删改查”就容易扯皮,比如“导出订单”、“批量审核”、“上下架”。
我们最终定的规则是:
- 标准增删改查用HTTP方法表达:
GET查、POST增、PUT整体改、DELETE删 - 复杂动作在资源下挂“动作子资源”:
POST /api/orders/export、POST /api/products/batch-audit - 不用
/api/getOrderList这种把方法写进URL的路由
为什么POST /api/orders/export比GET /api/orders/export?format=excel更合适?因为导出通常会带很多查询条件,用GET容易超出URL长度限制,而且导出行为本身有“创建一份文件”的语义,用POST更贴切。这是一次实际踩坑换来的结论——我们最初用的是GET,后来查询条件多到URL直接断了。
2.2 状态码:够用就好,别搞几十个
网上有很多RESTful状态码大全,什么402 Payment Required、405 Method Not Allowed、413 Payload Too Large,看得人头大。小团队不需要全套HTTP状态码,定得太细大家根本记不住,最后只会记住200和500。
我们实际只用这些:
| 状态码 | 使用场景 |
|---|---|
| 200 | 查询成功、操作成功 |
| 201 | 创建成功,配合POST使用 |
| 204 | 删除成功,一般不返回body |
| 400 | 参数错误、校验失败 |
| 401 | 未登录或Token失效 |
| 403 | 已登录但无权限 |
| 404 | 资源不存在 |
| 409 | 业务冲突,比如重复提交 |
| 500 | 未捕获的服务器异常 |
注意一个细节:业务失败不等于HTTP错误。比如“查了一个不存在的用户的订单列表”,资源本身存在(订单模块没问题),只是查询结果为空,这应该返回200加空数组,而不是404。如果每个业务上的“没有数据”都映射成HTTP错误码,前端要写一堆异常分支,接口层也会越来越脏。
2.3 路径设计:版本、前缀、大小写的习惯
RESTful风格下,URL是前后端对接的第一印象。我们在路径上做了几个硬性规定:
- 全部小写,单词间用连字符
-连接,不用下划线 - 所有接口统一前缀
/api - 第一段是版本号
/api/v1,即使当前只有一版,也先把位置占住 - 资源名一律用复数
至于为什么用连字符不用下划线,纯粹是为了浏览器和日志系统显示友好。下划线在某些前端框架的路由解析里会有转义问题,连字符则完全没有这个烦恼。
3. 共用接口设计:一套接口如何撑起多个端
3.1 共用而不是并行的接口模型
很多团队做多端支持时的第一反应是给每个端单独建一套接口,比如/admin/orders给管理后台,/user/orders给用户端。这样做短期内很直观,但长期看是灾难:订单模块的逻辑稍微改一下,你得同步改两套接口,漏改一个就是线上事故。
我们用的是共用接口模型,也就是同一个资源接口,通过不同的权限角色和参数裁剪来适配不同端。核心思路就一句话:接口只认角色,不认端。
比如订单列表接口:
GET /api/v1/orders?status=1&pageNum=1&pageSize=10- 管理端登录后,能看到的订单范围是全部用户
- 普通用户登录后,同样的接口只能看到自己的订单
区别不在URL,而在后端根据当前登录用户的数据权限自动拼接查询条件。管理端和用户端看到的字段也不同,这个通过返回值裁剪完成。
3.2 数据权限:共用接口最关键的一层
共用接口最怕的是越权。我们把权限模型分成了三档:
- 接口权限:决定能不能调这个接口(粗粒度)
- 数据权限:决定能看哪些数据(细粒度)
- 字段权限:决定能看哪些字段(最细粒度)
实现上用比较轻的方式:
// 伪代码,示意权限过滤思路 public PageResult<OrderVO> listOrder(OrderQuery query, LoginUser user) { // 接口权限判断 if (!permissionService.hasPermission(user, "order:list")) { throw new ForbiddenException(); } // 数据权限拼接 if (!user.isAdmin()) { query.setUserId(user.getId()); } // 查询后字段裁剪 List<OrderVO> data = orderService.query(query); if (!user.isAdmin()) { data.forEach(order -> order.setCustomerPhone(null)); } return new PageResult<>(data); }这是共用接口的精华所在。如果前端要把字段逻辑拆开,就退化成“一套代码给每个端调不同方法”了。共用接口的底线是:同一个资源的同一个操作,后端只有一个入口,所有端共享,但不同角色看到的数据范围不同。
3.3 字段裁剪:避免把不需要的数据全部糊给前端
共用接口必然带来一个问题:管理端需要用户手机号,用户端自己查自己不需要别人的手机号。我们的做法是定义VO(View Object),而不是直接把数据库实体类返回。
public class OrderVO { private Long id; private String orderNo; private BigDecimal amount; private Integer status; // 管理端可见,普通用户不可见 @RoleVisible(roles = {"admin", "operator"}) private String customerPhone; }这里用了注解标记,在序列化时根据当前用户角色动态决定字段是否输出。比起手写一堆if-else,这种方案维护成本低很多,新加字段时扫一眼注解就知道哪些角色可见。
3.4 关于“共用”的两个反例
也不是所有接口都适合共用。我们后来复盘,发现有两类接口不适合硬凑:
- 文件上传接口,因为各端的文件使用场景、大小限制都不同
- 报表统计接口,因为管理端的分析维度太多,强行共用会让查询参数膨胀到十几个,可读性和性能都很差
遇到这两种情况,单独建专用接口反而是更务实的方案。共用接口的目标是减少重复逻辑,不是把不相关的场景硬绑在一块。
4. 接口约定:五个必须提前定死的规范
4.1 统一响应体:一个壳子包住所有返回
前后端分离之后,前端同学最怕的就是“这个接口返回的是一个数组,那个接口返回的是一个对象,还有个接口出错时返回的是字符串”。响应体不统一,前期省事,后期想死。
我们的统一响应体结构非常简单:
{ "code": 0, "message": "success", "data": {} }code为业务错误码,0表示成功,非0表示具体错误message给前端展示用,后端不要把内部异常堆栈往这里塞data是业务数据的载体,可以是对象、数组、分页结构等
这里有个细节:业务错误码和HTTP状态码是两套维度。HTTP状态码只表示“请求链路是否正常”,业务错误码表示“业务逻辑是否成功”。比如库存不足,HTTP还是200,业务码是10086,这样前端可以根据业务码做提示,而不是HTTP层就报错导致无法区分参数问题和业务问题。
4.2 分页约定:参数名统一,返回结构固定
分页是前后端最容易吵架的地方。前端传page,后端返totalPage,前端传offset,后端返count——光分页这一件事就能让联调耗掉半天。
我们定的分页约定:
- 入参固定
pageNum和pageSize,pageSize上限100,防止有人一次拉全表 - 返回固定结构:
list(当前页数据)、total(总条数)、pageNum、pageSize
{ "code": 0, "message": "success", "data": { "list": [], "total": 156, "pageNum": 1, "pageSize": 10 } }分页排序也听坑,我们规定排序字段不能由前端直接传数据库列名,因为会有人传orderBy=id;drop table这种字符串。要么后端白名单校验,要么前端只能传固定的枚举值,比如createTime对应create_time,由后端映射。
4.3 时间格式:所有时间都是字符串
职位上有一个很经典的问题:后端返回Date对象,Jackson默认序列化成"2025-01-15T10:20:30.000+00:00"这种带时区格式,前端直接显示给人看就是乱码。我们还踩过时间戳、字符串、对象三种格式并存的坑,前端同事一度崩溃。
规定是死的,简单明了:
- 请求参数中的时间用
yyyy-MM-dd HH:mm:ss字符串 - 响应中的时间统一用
yyyy-MM-dd HH:mm:ss字符串 - 存库用
datetime类型,代码里用LocalDateTime
这样前端不用解析任何复杂格式,后端写个全局的Jackson配置就能搞定,所有接口保持一致。
4.4 命名和语义:别让前端猜你的意思
接口字段命名不统一,会显著拉高联调成本。比如一个表示状态的字段,有人叫status,有人叫state,还有人叫isActive。这种问题在接口评审时很难发现,要到前端写逻辑时才发现同一含义的字段换了三个名。
我们规定:
- 字段尽量用语义明确的名词,比如
orderStatus、amount、createdAt - 布尔类型用
is前缀,如isDeleted,不要混用deleteFlag、deleted、flag - 列表/分页返回的字段名不要用
data这种泛化的词,统一用list,避免和前端的Axios拦截器里response.data混淆
命名这块不涉及技术难度,纯粹靠自觉和评审。我后来总结出一个土办法:让前端把接到的JSON结构打印出来,当作代码注释一样贴在接口文档里,后端看一眼就知道自己哪里命名得不合群。
4.5 安全约定:认证、鉴权、防刷
安全约定不在最初的需求里,是后来被逼出来的。上了共用接口之后,多端复用同一个入口,防刷和鉴权就显得尤其重要。
- 认证统一用Token机制,前端在请求头带
Authorization: Bearer <token> - 写操作(POST、PUT、DELETE)要求做幂等校验,用的
Idempotent-Token请求头,后端用Redis判断是否已处理过 - 对于不需要登录的接口,单独配置白名单,比如登录接口本身、验证码接口
- 敏感操作记录操作日志,包含用户、时间、参数和结果
5. 实操过程:从接口定义到上线联调的全流程
5.1 第一步:接口定义先行
我们吃过最大的亏就是“边写边定接口”。后端写着自己的想象,前端画着自己的页面,一到联调发现两边理解的参数类型都对不上。后来强制规定:写代码前必须先出接口文档,接口评审通过后才允许进入开发。
这一步相当于把最终验收标准提前了。我们用的工具是Apifox(之前是Postman,但国内团队用其团队协作和Mock功能确实香),后端在Apifox里定义好接口,包括:
- 请求路径、方法、请求参数、类型、是否必填、示例值
- 响应体完整结构、字段说明
- 错误码说明
角色分工明确:后端负责定义接口,前端必须在后端确定的接口上进行开发,接口有变动必须通知所有受影响的前端,并在评审记录里留下变更说明。
5.2 第二步:Mock先行,前后端并行开发
接口定义好后,最怕的就是前端等后端的开发进度,导致项目延期。我们的做法是让Apifox自动生成Mock数据,前端在页面开发时直接调Mock服务。
Mock不是随便返回几个假数据就完事,我们总结了几个技巧:
- Mock数据必须符合接口约定,字段类型和长度跟真接口保持一致
- 列表类接口Mock数据量至少造200条,让前端能测分页、空态、加载态
- Mock数据中要特意包含边界情况,比如金额为0、状态为异常值、字符串超长等情况
前端用Mock数据把页面逻辑写完,后端同步按定义开发,等两边完了再切换到真实环境联调。这样原本需要串行等待10天的开发周期,直接压缩到一周内,因为前后端的并行度提上来了。
5.3 第三步:联调环境的规范切换
联调不是把前后端环境拼起来就算完,还得考虑环境切换时的配置管理。我们当时的做法是:
- 前端本地开发用
Vite的代理转发,代理目标是后端的dev环境 - 联调阶段统一使用
test环境,后端部署好全新的测试包,前端切代理到test - 部署工具用的Jenkins,后端的
dev和test环境用不同目录隔离,避免共用一个库一个Redis,因为测试数据会污染开发数据
这个环节最容易出的问题就是环境串了——前端本地连的数据库和后端本地连的数据库不是一个库,查出来的数据对不上,排查半天发现是环境变量配错了。
5.4 第四步:自动化测试守住接口稳定性
小公司通常没有专门的测试团队,接口质量全靠代码Review和人工点测。我们后来引入了一个很轻量的自动化测试方案:在Apifox里写接口测试用例,每次后端发版前跑一遍回归。
写这些用例的成本不高,但收益很大:
- 接口路径改了,用例挂了,能第一时间发现
- 响应字段删了,用例的断言失败,能提醒后端回归检查
- 状态码或业务码异常,用例直接报错
配合Jenkins,可以做到“后端构建成功后自动触发接口测试,测试通过才允许合并发布”。虽然比不上大厂的完整CI/CD流水线,但对小团队来说已经能挡住大部分低级回归问题了。
5.5 第五步:发布后的监控与反馈
上线不是终点。我们保留了接口层日志,记录每个接口的耗时、状态码和异常信息,配套做一个简单的定时任务聚合统计。不用上什么重量级的监控平台,因为小团队没人力天天看监控大屏,反而是在日志里加几个字段,出了问题时能快速定位是前端传参问题还是后端业务异常,就够用了。
6. 常见问题与排查技巧实录
6.1 必填参数校验缺失
联调期最多的问题就是必填参数忘了传。后端有时候为了赶进度,只写了查询逻辑没加参数校验,结果前端传了个null进去,数据库查询直接异常。
我们的排查技巧很简单:后端在所有接口入口统一做参数校验,用上Bean Validation:
public class OrderQuery { @NotNull(message = "用户ID不能为空") private Long userId; @Min(value = 1, message = "pageNum最小为1") private Integer pageNum = 1; @Max(value = 100, message = "pageSize最大为100") private Integer pageSize = 10; }任何接口在进入业务逻辑前先过校验层,不合格直接返回400加具体错误信息。定这个规范花了半天,但省下了无尽的“帮我看看为什么接口报错”的时间。
6.2 跨域问题:一次搞定,别反复配
前后端分离绕不开跨域。我们用Spring Boot,后端配置CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:*", "https://*.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里有个经验:allowCredentials(true)时前端不能再用*作为allowedOrigins,必须明确指定域名,否则请求被浏览器拦截。我们还真在这卡过半天,最后查阅文档才明白原因。
6.3 前后端“接口漂移”问题
所谓接口漂移,就是后端改了接口但忘了通知前端,前端还在按旧文档调。这在多人协作时很常见,尤其是后端一个人改了接口名,前端不知道。
我们后来做了两个约束:
- 接口变更必须走钉钉群通知,文案需要包含变更点、影响范围、预计改动时间
- 后端修改接口前,先确认是否有前端正在依赖这个接口。如果有,必须等前端完成改造或至少知道改造时间节点才能切走
这套约束听着像管理流程,但实际落地成本很低,关键是养成习惯。在群里喊一句“下单接口返回值里我加了couponId字段”,比事后线上出bug再排查爽多了。
6.4 重复提交的坑
共用接口上线后,我们遇到了一个前端点两次提交按钮导致重复下单的问题。前端做了按钮loading防重复,但网络慢的时候请求没有立刻返回,按钮没锁住,用户又点了一次,结果后端收到了两个一模一样的下单请求。
解决方式就是前面提到的幂等Token:
- 前端在进入下单页时,先请求一个
idempotentToken,存到前端变量 - 提交订单时,把Token放进请求头里
- 后端拿Token查Redis,如果存在,说明已处理过,直接返回第一次的结果,不再执行下单逻辑;如果不存在,存Token并执行下单逻辑
用了这个方案之后,重复下单的问题彻底解决。虽然一个Token机制代码量不多,但对下单这类写操作来说是刚需。
6.5 问题排查速查表
把常见问题整理成一张表,联调遇到问题先照表排查,能省不少沟通时间:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 接口返回401 | Token失效、过期或未携带 | 检查请求头是否带Authorization,Token是否过期 |
| 接口返回403 | 权限不足 | 检查角色配置,是否用了错误的账号调接口 |
| 返回400但消息不明确 | 参数校验没通过 | 查看是哪个参数不合格,前端检查传值 |
| 返回500 | 后端异常 | 让后端看日志,重点看NPE或SQL异常 |
| 返回空数组但应该有数据 | 数据权限过滤掉 | 检查登录用户是否有数据权限 |
返回结果里有null | 字段裁剪配置错误 | 检查VO注解配置,或数据库字段为空 |
| 前端乱码 | 时间格式不对 | 确认后端全局Jackson配置是否生效 |
| 接口跨域报错 | CORS配置不对 | 检查域名白名单,确认是否带credentials |
6.6 一些不那么技术、但更值得提醒的坑
技术之外,我感觉这套实践中最大的难点是“人心”。共用接口意味着大家不能再各写各的、各自为战。前端和后端必须愿意在日常开发中频繁对齐,后端要主动理解前端的消费场景,前端要接受后端统一的响应结构而不是“你按我的习惯返一个自定义格式”。
我们团队保持了一个习惯:每周五下午花一小时做接口Review,不是看代码,而是把本周新增的接口在浏览器控制台或者Apifox里拉出真实调用记录,挨个检查路径、参数、返回体、权限设置。这个习惯坚持了半年,发现的潜在问题少说十来个,包括一个管理端越权的严重漏洞。别觉得Weekly Review浪费时间,比起线上出事故再补救,这笔时间投入非常值。
6.7 基于经验的实操心得
说实话,在小公司做前后端分离这套东西,真正难的不是技术选型,不是RESTful规范,也不是共用接口模型,而是如何让一套规则在没有强管理手段的前提下被执行到底。
我的体会有几个:
- 规范一定是从需求里长出来的,不是拍脑袋想出来的。不用一步到位搞一个几十页的接口规范文档,先定五六个最关键的点,跑一个迭代再补。一次定太多大家记不住,最后什么都不执行
- 工具比人靠谱。接口文档只要是人记就一定会漂移,尽量用Apifox这种定义即文档、文档即Mock的工具,后端定义接口、前端用Mock,天然同步
- 接口评审要从一开始就坚持。哪怕需求紧,也要拉5分钟过一遍路径、参数、返回结构。省下评审的5分钟,联调阶段会多花50分钟来填坑
- 共用接口的“共用”是有限度的。硬把所有场景塞进同一个接口,最后谁都不爽。边界不清的时候,该拆就拆,独立接口没那么可耻
- 与业务方确定“先定接口再写代码”的规则后,前端后端的协作质量立竿见影。这是整个实践里性价比最高的一步
如果让我重新做一次这个小公司前后端分离的改造,我还是会选择这套方案:一个后端工程,以RESTful风格提供共用接口,用统一约定把响应体、错误码、分页、时间格式全部锁死,通过主动评审和工具链保证接口的生命力。它不炫技,但足够扎实,足够让一个三五人的技术团队省出时间来做真正有价值的事情——把业务做对,把用户体验做好。