全栈项目的分层与职责

一个可维护的全栈项目通常分四层:路由负责解析请求与参数校验,服务层承载业务规则,数据访问层封装数据库操作,模型层定义数据结构与约束。分层的意义不是形式主义,而是把「业务规则」从「框架细节」里剥出来——将来换数据库、加缓存、写单元测试,改动都被限制在一层之内。

分层的判断标准很简单:业务规则的代码里不应该出现框架特有的对象,服务层的函数应该能被直接调用并测试。
各层职责
  • 路由解析路径与查询参数、校验入参、调用服务、格式化响应。
  • 服务业务规则与事务边界,是最值得写测试的地方。
  • 数据访问只处理查询与写入,不掺业务判断。
  • 模型字段类型、唯一约束、关联关系。
全栈开发配图:浏览器、服务器与数据库三层抽象分层与数据流
三层之间只通过数据契约通信,任何一层都可以独立替换。

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,可以防止有人用超大分页把数据库打满。这类防御性写法应当固化成习惯,而不是等有人提交了异常参数再补。

数据库建模与查询

建模的第一步是找出实体与关系,再决定字段约束。主键用自增整数或全局唯一标识取决于是否需要对外暴露:可枚举的整数会泄漏业务量,也给爬虫留下遍历空间,对外接口更适合用不可预测的标识。索引要建在查询条件与排序字段上,而不是凭感觉给每一列都加,写多读少的场景下过多索引反而拖慢写入。

需求做法理由
分页查询游标或「排序字段 + 主键」复合条件避免大偏移量扫描
唯一性约束交给数据库的唯一索引应用层判断存在竞态
列表排序为排序字段建索引排序是高频操作

深分页是最容易被忽略的性能问题:偏移量很大时数据库仍需扫描并丢弃前面的行,页码越深越慢。用「上一页最后一条记录的排序值」作为游标就能稳定在常数开销,这也是主流列表接口的推荐做法。

鉴权、日志与错误处理

鉴权建议用无状态令牌:登录成功后签发带过期时间与签名的令牌,后续请求放在请求头里校验,服务端不保存会话,天然支持横向扩容。权限判断不要只做在界面层——前端隐藏按钮只是体验优化,接口必须独立校验,否则一个改包的请求就能越权。

服务端基本功
  • 统一错误中间件兜住所有未捕获异常,输出稳定的错误结构并记录堆栈。
  • 结构化日志带上请求标识与耗时,便于按一次请求串起全链路。
  • 敏感信息脱敏日志里不打印密码、令牌与完整手机号。
  • 参数校验前置在路由层拒绝非法入参,服务层可以假设数据合法。

部署与运行保障

部署的最小闭环是:构建产物可复现、配置来自环境变量、进程异常可自动重启、日志能集中查看。把端口号、数据库地址、密钥全部走环境变量,同一份镜像就能在测试与生产之间切换;把配置写死在代码里,迟早会遇到「本地能跑线上报错」的排查困境。

上线之后至少补齐三项观测:接口耗时与错误率、进程内存与句柄数、数据库慢查询。很多故障在爆发前都有前兆,例如内存曲线持续抬升——那通常是缓存没有上限或者事件监听没有移除,等到进程被系统杀掉才处理就晚了。