
欢迎来到Bokey的空间🌼
加载中...
🚀关于通用接口系统设计分享
关于我实现通用接口的设计理念,希望对大家有启发

🚀通用接口系统设计分享
设计背景
在日常后端开发中,你会发现一个很现实的问题:
80% 的接口,本质上都是 CRUD(增删改查)
但传统开发方式却是这样的:
每个模块写一套 Controller / Service / Router
重复处理分页、鉴权、字段过滤、事务
每个接口风格还不统一
久而久之,就会出现这些问题:
❌ 代码大量重复
❌ 修改成本高(改一个逻辑要改 N 个地方)
❌ 权限控制分散
❌ 接口风格混乱
❌ 开发效率低
💡所以我当时在想一个问题:
既然 CRUD 是“高度模式化”的,那为什么还要一遍遍手写?
于是有了这套系统的核心目标:
🚀 快速开发:减少重复代码
🔧 高度可配置:支持不同业务场景
🔐 权限解耦:统一中间件控制
♻️ 高复用性:通用 CRUD + 关联查询
⚙️ 可扩展性:允许局部自定义
因此我开发了自己的这套通用接口
这套通用后端好用的点在于:
- 把“接口实现” → “接口声明”
- 把“业务逻辑” → “参数加工”
这样基本的接口建表后复制一下配置就能快速完成接口,同时也不会缺失安全性,鉴权或者部分特殊处理都可以在配置中实现,可以非常快速的完成接口开发。
最重要的是,通用接口仍然可以在后期无痛转化成处理专项场景的接口,极大的满足了快速开发、可拓展性强的开发需求
🔍 与传统开发方式对比
| 维度 | 传统方式 | 通用接口方案 |
|---|---|---|
| 开发方式 | 手写 Controller | 配置驱动 |
| 权限控制 | 分散在业务代码 | 中间件统一治理 |
| 查询结构 | 不统一 | 标准化 where/order/include |
| 事务管理 | 手动控制 | 路由声明式开启 |
| 可维护性 | 低 | 高 |
| 可治理性 | 低 | 高 |
| 可读性 | 低 | 高 |
核心设计思想
这套系统本质上只做了一件事:
把“接口实现”变成“接口配置”
一句话总结:
👉 配置即接口,积木式开发
✨ 设计原则拆解
- 减少重复劳动
CRUD 是标准化行为 → 不应该重复写代码 - 配置即接口
接口不是写出来的,而是“声明出来的” - 可治理
对高风险点统一控制(字段权限、查询范、事务一致性) - 可扩展
允许“局部自定义”,但不破坏整体结构
总体架构:配置驱动的通用接口链路
通用接口的核心是一条稳定的处理链路:
- Route Config(配置层):用统一结构描述每个公开接口:HTTP 方法、路径、前置中间件、处理器、后置中间件,以及固定参数(如主模型、关联模型等)。
- Dynamic Router(路由装配层):启动时读取所有配置,自动把它们挂载为 Express 路由。
- Base Controller(通用控制器层):提供通用的
get / create / update / delete / relevance等处理器。 - Base Service(通用服务层):封装 ORM 访问与通用数据操作语义(分页、include、事务、关联同步等)。
- Base Middlewares(治理层):围绕“请求输入/权限/事务”提供可复用的中间件工厂,按路由配置插拔。
这套设计主要突出:
- 配置即接口:一个接口不是“写出来的”,而是“配置出来的”(声明式)。
- Controller、Middleware 积木化:把能力拆成可复用积木,通过拼接完成接口编写,而不是复制粘贴。
🧩 配置即接口:核心结构设计
在我的通用接口中,主要的“接口配置结构”如下(说明式代码):
TypeScript
export interface BaseRouteConfig {
method: "get" | "post" | "put" | "delete"; // HTTP方法
externalPath: string; // 路由别名
preMiddlewares: RequestHandler[]; // 前置中间件数组
handler: RequestHandler; // 使用的控制器
fixedParams?: {
MainModel: string;
RelevanceModels?: string[] | string;
targetAlias?: string;
}; // 内部通用路由需要的params参数数组
postMiddlewares: RequestHandler[]; // 后置中间件数组
}
这种结构的本质是:把“接口的形态”用配置描述,把“接口的通用能力”下沉到框架层,把“差异化逻辑”留在中间件或参数工厂中。
为了更直观地体现“配置即接口”,下面是一段“路由配置如何长出来”的示意:
ts
// 1) 声明一个接口 = 声明一条配置
const example: BaseRouteConfig = {
method: "put",
externalPath: "/resource/update",
fixedParams: { MainModel: "Resource" },
preMiddlewares: [
// 鉴权中间件:声明该接口需要什么角色/登录态
middlewares.auth.authJudgeFactory({ roleCheck: ["admin"]}),
// 字段治理中间件:例如禁止客户端更新 id
middlewares.base.limitRequestPayloadFactory({ deleteFields: ["id"]}),
// 开启事务中间件:让后续写操作运行在同一个 transaction 中
middlewares.base.setTransaction,
],
// 控制器中间件:不写业务 CRUD,只复用通用 handler
handler: baseController.update,
postMiddlewares: [
// 提交事务中间件
middlewares.base.commitTransaction,
// 返回数据中间件
middlewares.network.response,
],
};
再进一步,“配置即接口”带来的一个工程体验是:新增接口时,更多是在“选择并排列积木”,而不是重复造轮子。
ts
// 2) 同一套积木,拼接出不同接口形态
const listApi: BaseRouteConfig = {
method: "get",
externalPath: "/resource/list",
fixedParams: { MainModel: "Resource", RelevanceModels: ["Owner"] },
preMiddlewares: [
middlewares.auth.authJudgeFactory({}),
// 参数改写积木:把当前用户写入 where,实现“只查自己的”
middlewares.base.changeOrCheckRequestPayloadFactory({ changeFields: { "where.ownerId": "req.user.id" } }),
],
handler: baseController.getAll,
postMiddlewares: [middlewares.network.response],
};
const createApi: BaseRouteConfig = {
method: "post",
externalPath: "/resource/create",
fixedParams: { MainModel: "Resource" },
preMiddlewares: [
middlewares.auth.authJudgeFactory({}),
middlewares.base.changeOrCheckRequestPayloadFactory({ changeFields: { ownerId: "req.user.id" } }),
middlewares.base.setTransaction,
],
handler: baseController.create,
postMiddlewares: [ middlewares.base.setTransaction, middlewares.network.response,],
};
中间件式 Controller:把“拼装请求”与“调用通用服务”标准化
中间件式 Controller 的职责不是写业务逻辑,而是做“请求 → 查询/写入参数”的标准化拼装:
- 读取统一查询结构:将 query/body 解析为结构化参数(例如 where、order、attributes、分页等)。
- 构建 include(关联查询):
- 支持普通关联 include
- 支持递归关联 include(把多层关联以嵌套结构表达)
- 对 include 的 attributes/required/through/where 等提供统一可控入口
- 可插入的参数工厂:允许某些路由在进入通用 service 前,对参数做二次加工(例如默认排序、按当前用户过滤、补充 include 等)。
- 事务感知:当路由启用了事务中间件,Controller 在创建/更新/删除/关联写入时会自动携带 transaction。
这样做的意义在于:将“数据接口的通用形态”固定下来,把变化点收敛为可治理的参数变换。
从“积木化”的视角看,Controller 更像一个稳定的“接口模板”,它不关心业务,只负责:
- 把请求解析成统一的数据操作参数(例如 where/order/page/include)
- 在必要时允许外部注入一个“参数加工器”(把变化点变成可控插件)
- 将最终参数交给通用 Service 执行,并把结果挂到响应上下文中
说明式伪代码如下:
ts
// 3) Controller 的职责:标准化拼装 + 可插拔参数加工
async function getAllController(req, res, next) {
const query = parseQuery(req); // where/order/page/include...
const factory = req.controllerParamFactory ?? (x => x);
res.data = await baseService.getAll(await factory({
MainModel: req.params.MainModel,
...query,
}));
next();
}
通用 Service:把 ORM 操作抽象成稳定语义
通用 Service 的设计思路是“提供稳定语义的能力,而非暴露 ORM 细节”:
- 通用查询:支持分页与非分页两种模式,并对联表统计的常见问题(如 count 偏大)做统一处理策略。
- 通用创建:支持单条与批量创建,保持调用方 API 一致性。
- 通用更新:支持按条件更新,并支持在外部事务中运行。
- 通用删除:提供软删除/硬删除(force)语义的统一入口。
- 通用关联同步:
- 统一入口根据关联类型自动选择同步策略
- 覆盖式语义更适合“后台配置/管理”场景(一次提交即以当前提交为准)
- 支持在同步关系的同时附带更新某些目标字段(以满足“关系表带额外字段”的场景)
对外表现为:业务方只关心“要做什么”(查询/创建/更新/删除/关联),不需要关心 ORM 细节和边界处理。
治理型 Middlewares:把“安全与一致性”做成可插拔能力
通用接口最容易出现的问题是:字段越权、查询越权、批量写入缺少事务、以及参数不一致导致的数据脏写。为此引入了一组“治理型中间件工厂”:
-
请求字段约束(limitRequestPayloadFactory)
- 删除敏感字段(例如不允许客户端更新 id)
- 替换/注入字段(例如强制写入某个固定值)
- 过滤 attributes(避免敏感列被查询出来)
- 可按角色生效(仅对某些角色启用更严格/更宽松的限制)
-
请求参数改写/校验(changeOrCheckRequestPayloadFactory)
- 把当前登录用户信息写入查询条件或请求体(实现“只能操作自己的数据”)
- 校验某些字段只能是允许的值/符合正则/满足函数校验
-
存在性校验(checkExistsFactory)
- 在进入写操作之前做数据存在性验证,提前失败,提升可读性与错误一致性
-
事务控制(setTransaction / commitTransaction)
- 在路由层声明“该接口需要事务”
- 统一把 transaction 放在请求上下文中,下游自动感知并复用
这些中间件的共同目标是:把“安全性、数据一致性、权限边界”从业务代码中抽离出来,成为路由级可复用的约束。
扩展点:如何在不破坏通用架构的前提下满足差异化需求
通用接口并不追求覆盖 100% 场景,而是提供明确的扩展方式:
- 在配置层插入 preMiddlewares/postMiddlewares:用中间件实现权限、字段约束、额外校验、外部服务调用等。
- 参数工厂(controllerParamFactory):在进入通用 service 前对查询参数做二次加工,适合“同一模型但不同视图/筛选/排序”的场景。
- 少量专用接口与通用接口并存:当业务行为型接口(如支付回调)不适合 CRUD 时,独立实现专用 router/controller/service,不与通用接口耦合。
这体现了一种工程实践:用通用接口解决规模问题,用专用接口解决语义问题。
🎯适用场景与取舍
✅适用场景
- 后台管理系统常见的数据管理页面(列表/详情/创建/编辑/删除)
- 需要快速开放大量资源接口的中小型项目
- 需要对接口进行统一治理(字段、权限、事务)以降低安全风险
⚖️ 取舍
- 通用接口偏“数据导向”,不适合强业务语义的流程型接口
- 需要约束团队使用方式(例如关联同步的覆盖式语义、字段限制的统一约定)
发布于
2025-05-18
更新于
2026-04-11
类目
作者
Bokey
版权协议