从 Express 老项目到 NestJS + Docker:一次车辆管理系统的渐进式重构 从 Express 老项目到 NestJS Docker一次车辆管理系统的渐进式重构不是推倒重写而是让旧项目一步步变成可交付的全栈项目。1. 为什么要重构这个项目是我之前写的车辆管理系统——React 前端 Express 后端 MongoDB。功能不复杂用户管理、部门管理、菜单管理、角色管理、订单管理、Dashboard 图表。能跑但有几个问题Express 路由文件里既写业务逻辑又写数据库操作没有分层。没有统一的错误处理、没有参数校验、没有模块化。本地跑需要装 MongoDB换台电脑就跑不起来。想拿来做全栈项目展示但能跑和别人也能跑是两回事。一开始想过直接用 NestJS 从头写但转念一想——前端还在用旧接口如果后端接口签名变了前端也得跟着改改着改着两边都崩了。最后选了一条更稳的路渐进式重构。旧 Express 不动新 NestJS 在旁边一个接口一个接口地迁。每迁完一个就跑脚本验证前端完全不用改。2. 我没有直接推倒重写很多人重构的第一反应是开个新分支从零开始写。听起来爽但我之前试过写到一半发现旧接口里有一堆隐式约定——字段名拼错了、响应结构不一致、某些接口没有 msg 字段。如果推倒重写这些坑全得重新踩一遍。所以这次的做法是旧 Express 保留在仓库根目录index.cjsrouter/models/新 NestJS 放在nest-server/子目录两套服务可以同时存在互不干扰前端只需要把 API 地址从 3000 换成 3001其他不动这样每一步都能回滚。如果哪个接口迁移出了问题切回旧端口就行。3. 第一步不是写代码而是冻结接口契约这一步最容易被跳过但其实最重要。在开始写 NestJS 之前我先做了两件事把旧 Express 的所有接口整理成一份清单docs/API_CONTRACT_CHECKLIST.md写了一个 shell 脚本自动验证接口响应scripts/check-api-contract.sh整理的过程中发现了不少历史特色/users/getPermissionList的响应里字段名是meg不是msg。应该是当初写错了但前端已经用了这个字段。/roles/updata/permission——对是updata不是update。/order/vehiclelist的响应里压根没有msg字段其他接口都有。token 放在Authorizationheader 里但不带Bearer前缀。直接就是裸 token。这些问题要不要修不修。重构的目标是迁移不是修 bug。如果一边迁移一边修旧问题前端就得跟着改复杂度会爆炸。先原样复刻等全部迁完、前端接入成功之后再单独开 PR 修这些历史遗留。4. NestJS 怎么迁迁移顺序不是随意定的我按风险从低到高排/health, /health/db ← 先确认框架能跑 /user/login ← 鉴权是所有接口的前置条件 /users/getUserInfo ← 登录后第一个请求 /users/getPermissionList ← 动态菜单依赖它 GET 查询接口用户、部门、菜单、角色、订单、Dashboard POST 写操作create、edit、delete 上传/users/upload 导出/order/export为什么这么排/health是最简单的只是确认 NestJS 项目结构没问题。登录必须先迁因为后续所有接口都依赖 token。GET 查询不改数据库即使写错了也不会污染数据。POST 写操作会改数据出错可能把测试数据搞乱。上传和导出是特殊接口一个处理文件一个返回二进制流复杂度最高放最后。每迁完一组接口就跑一遍scripts/check-nest-api-contract.sh确认没有回归。5. 为什么查询接口和写接口要分开这一步不能急。GET 查询接口的好处是写错了不会造成任何后果。数据库里该什么样还是什么样最多就是返回格式不对前端显示有问题改了再试就行。但写操作不一样。POST /users/create如果参数处理不对可能会往数据库里插一条脏数据。POST /order/delete如果条件写错了可能删多了。所以写操作单独一轮用测试数据验证完再合并。上传和导出就更特殊了/users/upload需要处理 multipart/form-dataMulter 配置路径、文件名规则都要对。/order/export返回的是 Excel 文件流application/vnd.openxmlformats不是 JSON。响应拦截器不能对它做JSON.parse。这两个放最后避免一开始就被文件处理的问题卡住。6. 最容易踩坑的几个点整个重构过程中翻车最多的不是怎么写 NestJS而是这些细节meg 不是 msg/users/getPermissionList返回的权限列表旧 Express 用的字段名是meg。我一开始想当然地写了msg结果前端菜单渲染不出来。排查了半天才发现这个拼写差异。updata 不是 update/roles/updata/permission这个路由updata是旧代码写错了。但前端调的就是这个 URL所以 NestJS 也得保持这个拼写。改了就 404。token 不带 Bearer很多教程里 JWT 认证用的是Bearer token格式。但这个项目旧 Express 的实现是直接把 token 裸放在Authorizationheader 里。NestJS 的 Guard 如果按标准写split(Bearer )[1]就会解析失败。Docker 里 MongoDB 不能用 127.0.0.1:27018容器内部的 NestJS 连 MongoDB不能用宿主机的映射端口。要用 Docker Compose 的服务名mongodb://mongo:27017/MyManager ← 容器内用这个 mongodb://127.0.0.1:27018/MyManager ← 宿主机用这个这个错误不会报连接被拒绝而是会卡住很久然后超时排查起来很痛苦。浏览器访问前端时API 不能写 nest-api:3001前端是在浏览器里跑的浏览器在宿主机上。nest-api是 Docker 内部的服务名浏览器解析不了。API 地址必须是http://localhost:3001。集合名是 userslists 不是 usersMongoDB 里的集合名不是你以为的users、orders而是userslists、orderlists。如果 Mongoose Schema 没有指定 collection 名NestJS 会按自己的规则推导结果查出来是空的。本机 Nest 和 Docker Nest 同时跑端口 3001 只能被一个进程占用。如果本地npm run start:dev还开着Docker 容器就起不来。反过来也一样。EADDRINUSE这个错经常忘了是这个原因。7. Docker 化把项目变成可交付能跑和别人也能跑是两回事。本地开发时我的电脑上有 MongoDB、有 Node.js、有正确的环境变量。但换一台电脑或者交给别人看就得重新配置一遍。Docker 解决的就是这个问题。Docker 化分了四步第一步Docker 化 MongoDB最简单的一步。MongoDB 官方镜像拿来直接用把端口映射到 27018避免和本机 MongoDB 冲突数据用 volume 持久化。旧数据怎么办mongodump导出mongorestore导入。不复杂但有一点要注意dump 文件不要提交到 Git。数据库里有用户密码虽然是 MD5不适合公开。第二步Docker 化 NestJS用多阶段构建FROM node:20-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omitdev COPY --frombuilder /app/dist ./dist CMD [node, dist/main]第一阶段装所有依赖、编译 TypeScript。第二阶段只装生产依赖、拷贝编译产物。最终镜像不含 TypeScript 源码和 devDependencies体积小很多。第三步Docker 化 React 前端前端构建完就是一堆静态文件用 Nginx 托管就行FROM node:20-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf CMD [nginx, -g, daemon off;]有一个关键配置前端的 API 地址必须在构建时注入VITE_BASE_APIhttp://localhost:3001。因为浏览器直接请求后端不经过 Nginx 代理。相应地NestJS 要开启 CORS。第四步docker-compose 编排services:mongo:image:mongo:7ports:[27018:27017]volumes:[mongo-data:/data/db]nest-api:build:{context:./nest-server}ports:[3001:3001]environment:MONGODB_URI:mongodb://mongo:27017/MyManagerdepends_on:[mongo]web:build:{context:../react-manager}ports:[8080:80]depends_on:[nest-api]注意web的 build context 是../react-manager——前端仓库是后端仓库的兄弟目录不是子目录。8. 空白电脑如何启动拿到代码后的完整流程# 1. 拉代码在同一个父目录下gitclone https://github.com/lichenyang5/react-manager-server.gitgitclone https://github.com/lichenyang5/react-manager.git# 2. 切到重构分支cdreact-manager-servergitcheckout refactor/backend-first# 3. 启动 MongoDB先把数据准备好dockercompose up-dmongo# 4. 导入数据如果你有 dump 文件的话mongorestore--urimongodb://127.0.0.1:27018/MyManager\./docker/mongo/dump/MyManager--drop# 5. 一键启动全部dockercompose up-d--build# 6. 验证curlhttp://localhost:3001/health# 打开 http://localhost:8080用 admin / 111111 登录如果没有数据库 dump容器能启动但登录会失败因为数据库里没有 admin 用户。这不是 bug是数据还没导入。9. 最后项目变成什么样react-manager-server/ ├── index.cjs # legacy Express保留不再维护 ├── nest-server/ # 主后端NestJS ├── docker-compose.yml # 一键编排 └── scripts/ # 契约检查脚本 react-manager/ ├── src/ # React 前端 ├── Dockerfile # Docker 构建 └── nginx.conf # 生产 Nginx 配置三个容器容器职责访问地址react-manager-mongoMongoDB 数据库localhost:27018react-manager-nest-apiNestJS API 服务localhost:3001react-manager-webReact 前端Nginxlocalhost:8080旧 Express 还在但已经是 legacy。新功能只在 NestJS 上开发。前端从 Vite 开发代理切换到了直连http://localhost:3001NestJS 通过 CORS 支持跨域。有一个自动化脚本scripts/check-nest-api-contract.sh验证 26 个接口点确保迁移没有遗漏和回归。10. 这次重构最大的收获不写鸡汤说具体的先锁契约再重构。没有契约清单和自动验证脚本重构就是盲人摸象。你以为改完了其实漏了一个字段、少了一个 header、响应码不对。脚本能在 10 秒内告诉你哪里不对。把大重构拆成小步。一次性迁移 20 个接口出了问题不知道是哪个改坏的。一次迁 3-5 个跑一轮脚本心里有底。Docker 化的价值不是炫技。它解决的是换一台电脑还能不能跑这个最基本的问题。如果你的项目只有你自己能跑起来那它只是个本地 demo。处理历史包袱比写新代码难。meg、updata、裸 token、不规范的集合名——这些不是你的错但重构时你得兼容它们。正确和能用之间要选后者至少在迁移阶段。让项目从能跑变成别人也能跑。README 写清楚了、Docker 配好了、数据迁移有文档了、验证有脚本了——这才是一个可以交出去的项目。