全栈项目的分层与职责
一个可维护的全栈项目通常分四层:路由负责解析请求与参数校验,服务层承载业务规则,数据访问层封装数据库操作,模型层定义数据结构与约束。分层的意义不是形式主义,而是把「业务规则」从「框架细节」里剥出来——将来换数据库、加缓存、写单元测试,改动都被限制在一层之内。
分层的判断标准很简单:业务规则的代码里不应该出现框架特有的对象,服务层的函数应该能被直接调用并测试。
- 路由解析路径与查询参数、校验入参、调用服务、格式化响应。
- 服务业务规则与事务边界,是最值得写测试的地方。
- 数据访问只处理查询与写入,不掺业务判断。
- 模型字段类型、唯一约束、关联关系。
Express 接口设计与参数校验
接口设计先定返回结构:成功时统一返回数据与元信息,失败时统一返回错误码与可读消息。这样前端只需要写一份解析逻辑,错误提示也能直接展示。Express 的中间件链天然适合做横切关注点,路由只负责把校验后的数据交给服务层。
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/articles", async (req, res, next) => {
try {
const page = Math.max(1, Number(req.query.page) || 1);
const size = Math.min(50, Number(req.query.size) || 20);
const { rows, total } = await articleService.list({ page, size });
res.json({ ok: true, data: rows, meta: { page, size, total } });
} catch (err) {
next(err); // 交给统一的错误中间件
}
});
注意分页参数的上限与下限都被夹住了:size 最大 50,可以防止有人用超大分页把数据库打满。这类防御性写法应当固化成习惯,而不是等有人提交了异常参数再补。
数据库建模与查询
建模的第一步是找出实体与关系,再决定字段约束。主键用自增整数或全局唯一标识取决于是否需要对外暴露:可枚举的整数会泄漏业务量,也给爬虫留下遍历空间,对外接口更适合用不可预测的标识。索引要建在查询条件与排序字段上,而不是凭感觉给每一列都加,写多读少的场景下过多索引反而拖慢写入。
| 需求 | 做法 | 理由 |
|---|---|---|
| 分页查询 | 游标或「排序字段 + 主键」复合条件 | 避免大偏移量扫描 |
| 唯一性约束 | 交给数据库的唯一索引 | 应用层判断存在竞态 |
| 列表排序 | 为排序字段建索引 | 排序是高频操作 |
深分页是最容易被忽略的性能问题:偏移量很大时数据库仍需扫描并丢弃前面的行,页码越深越慢。用「上一页最后一条记录的排序值」作为游标就能稳定在常数开销,这也是主流列表接口的推荐做法。
鉴权、日志与错误处理
鉴权建议用无状态令牌:登录成功后签发带过期时间与签名的令牌,后续请求放在请求头里校验,服务端不保存会话,天然支持横向扩容。权限判断不要只做在界面层——前端隐藏按钮只是体验优化,接口必须独立校验,否则一个改包的请求就能越权。
- 统一错误中间件兜住所有未捕获异常,输出稳定的错误结构并记录堆栈。
- 结构化日志带上请求标识与耗时,便于按一次请求串起全链路。
- 敏感信息脱敏日志里不打印密码、令牌与完整手机号。
- 参数校验前置在路由层拒绝非法入参,服务层可以假设数据合法。
部署与运行保障
部署的最小闭环是:构建产物可复现、配置来自环境变量、进程异常可自动重启、日志能集中查看。把端口号、数据库地址、密钥全部走环境变量,同一份镜像就能在测试与生产之间切换;把配置写死在代码里,迟早会遇到「本地能跑线上报错」的排查困境。
上线之后至少补齐三项观测:接口耗时与错误率、进程内存与句柄数、数据库慢查询。很多故障在爆发前都有前兆,例如内存曲线持续抬升——那通常是缓存没有上限或者事件监听没有移除,等到进程被系统杀掉才处理就晚了。
性能与安全要点
性能上优先做三件事:给高频查询加索引并核对执行计划、给可缓存的读接口加缓存并设置合理的过期时间、把耗时任务从请求链路里挪到后台队列。第三点尤其重要——在请求里同步做图片处理、批量导出这类重活,会直接拖垮整个服务的响应时间,而这些工作完全可以异步完成后再回调通知。
安全上要守住几条底线:所有查询使用参数化写法而不是字符串拼接,从根本上消除注入风险;对上传文件做类型与大小校验,存储时重命名并放在不可执行的目录;对外接口加限流与超时,避免被单点流量打满;依赖定期更新,已知高危漏洞要及时升级。
// ❌ 字符串拼接:存在注入风险
db.query(`SELECT * FROM users WHERE name = '${name}'`);
// ✅ 参数化查询:驱动会正确处理转义
db.query("SELECT * FROM users WHERE name = ?", [name]);
最后是接口幂等性:支付、下单这类操作要支持重复请求而不产生副作用,常见做法是要求客户端携带唯一的请求标识,服务端用数据库唯一约束去重。这在网络抖动导致重试的场景下必不可少。
小结与延伸阅读
全栈能力的关键不是会写前后端两套代码,而是理解数据在层与层之间如何流动,以及各层应该承担什么职责。
- 接口调试与异步基础,见《JavaScript 核心进阶:异步与模块化》。
- 前端侧实现,读《React 18 实战:组件化开发》或《Vue 3 组合式 API 开发指南》。
- 类型契约贯通前后端,读《TypeScript 类型系统精讲》。