一、老Express项目迁移的痛点与核心思路

1.1 Express项目的“老年病”

很多早年做的Node.js项目,大多用Express搭起来,刚上线的时候代码整洁,功能清晰。但架不住业务越堆越多,后来加的接口、逻辑随便乱塞,路由、业务计算、数据库操作全缠在一个文件里,改个用户接口要翻半天注释,团队新成员根本摸不清脉络,维护起来像拆没有说明书的盲盒。想升级到更规范的框架?又怕全量重构把现有核心业务搞崩——服务器跑着订单支付、用户登录这些关键功能,断服务一天损失几万块,谁都不敢轻易动手。

1.2 NestJS为什么是好的选择

NestJS是基于Node.js的后端框架,它不像Express那样太“松散”,而是定了明确的架构分层:路由层管请求响应、服务层管业务逻辑、模块负责聚合功能,还自带依赖注入,能把代码拆成一个个独立可复用的单元,后续维护、团队协作都轻松很多。但很多人怕“一迁就死”,其实不用急,渐进式迁移就是最优解——不用一次性全改,一点点来,业务始终跑着,风险完全可控。

二、渐进式迁移的第一步:路由层的平滑共存

迁移的第一步绝对不能碰核心业务逻辑,先从最外层的路由入手,让老Express的路由和新NestJS的路由能同时跑在同一个服务上,既不影响老用户,又能逐步验证新框架的优势。

2.1 混合应用模式的实操

NestJS官方就支持把它的应用嵌套进Express里,这样你就能用老的Express实例承载旧路由,再把NestJS的新路由挂载上去,相当于两条腿走路。咱们直接上完整可运行的代码示例:

// 技术栈:NestJS 9 + Express 4(混合应用模式,适配渐进式迁移)
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import { AppModule } from './app.module';

// 1. 初始化老项目的Express实例,保留原有所有配置(比如中间件、监听端口)
const expressApp = express();

// 2. 异步创建Nest应用,用Express适配器把它绑定到现有的Express实例上
async function startMigration() {
  // 用Express适配器包裹Nest,让Nest的路由能挂在现有Express上
  const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
  // 初始化Nest应用,此时它的路由已经整合进Express了
  await nestApp.init();

  // 3. 启动服务,老Express路由和新Nest路由同时生效,完全不冲突
  const SERVER_PORT = 3000;
  expressApp.listen(SERVER_PORT, () => {
    console.log(`✅ 服务已启动,端口:${SERVER_PORT}`);
    console.log(`📌 老Express路由:http://localhost:${SERVER_PORT}/api/old/user/list`);
    console.log(`📌 新Nest路由:http://localhost:${SERVER_PORT}/api/new/user/list`);
  });
}

startMigration();

配套的Nest核心模块和控制器代码,保证你能直接复用:

// app.module.ts(Nest的模块定义,相当于功能包的集合)
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

// 把用户相关的控制器和服务注册到模块里,这是Nest的强制规范,让代码逻辑清晰
@Module({
  controllers: [UserController], // 负责处理请求的路由层
  providers: [UserService] // 负责处理业务逻辑的服务层
})
export class AppModule {}
// user.controller.ts(Nest的新路由,路径和老路由分开,避免冲突)
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';

// 路由前缀是/api/new,和老路由的/api/old彻底区分,安全又好管理
@Controller('api/new/user')
export class UserController {
  // 依赖注入:自动把UserService注入进来,不用手动创建实例,简化代码
  constructor(private readonly userService: UserService) {}

  // 处理GET请求,对应完整路径/api/new/user/list
  @Get('/list')
  async getNewUserList() {
    // 调用服务层逻辑,这里的业务逻辑是新的,之后可以替换老代码
    return this.userService.fetchUserList();
  }
}
// user.service.ts(Nest的服务层,纯业务逻辑,和路由完全解耦)
import { Injectable } from '@nestjs/common';

// 标记这是一个可注入的服务,供其他类调用,是依赖注入的核心基础
@Injectable()
export class UserService {
  // 模拟业务逻辑,实际项目里可以换成数据库操作、第三方接口调用等
  fetchUserList() {
    return [
      { id: 1, name: '张三', age: 25, source: 'Nest新服务' },
      { id: 2, name: '李四', age: 30, source: 'Nest新服务' }
    ];
  }
}

你还可以保留老Express的路由,原封不动不用改,安心过渡:

// 老Express的路由,完全保留,继续服务老用户
const express = require('express');
const oldRouter = express.Router();

// 老用户列表接口,路径是/api/old/user/list,和新路由无冲突
oldRouter.get('/list', (req, res) => {
  // 原有业务逻辑,暂时不变,后续再逐步替换成Nest的服务
  res.json([
    { id: 1, name: '张三', age: 25, source: 'Express老路由' },
    { id: 2, name: '李四', age: 30, source: 'Express老路由' }
  ]);
});

module.exports = oldRouter;

2.2 第一步的验证要点

做完这一步,你只需要做两件事:启动服务,分别访问老、新两个接口,确认返回值都正常、没有报错,也不会打断老用户的使用。这一步的核心就是“共存”,只要能同时跑,就算成功,不用急着改别的。

三、迁移的第二步:逐步抽离业务逻辑到Nest服务

当你确认路由层的共存没问题了,就可以开始把老Express里的业务逻辑,一点点抽成Nest的服务,这就是依赖注入的好处——不用改路由,只要替换服务里的代码就行,风险极低。

3.1 场景一:先迁非核心模块

比如用户列表、商品分类这种非核心的模块,先拿它们练手,等熟悉了Nest的架构,再碰支付、订单这种核心功能。比如你可以把老Express路由里的业务逻辑,复制到Nest的UserService里,然后测试新路由的返回值和老的完全一致,没问题的话,就可以把老路由的逻辑逐步停用,让新路由承接流量。

3.2 场景二:用依赖注入解耦

举个例子,老Express里的用户逻辑是硬编码在路由里的,改的时候容易出错,而Nest的服务是独立的,依赖注入让你可以随时替换它的实现。比如你要做测试,只要换个Mock的UserService,不用改任何其他代码,这在老Express里根本做不到——你得改一堆路由里的判断,复杂度直线上升。

四、渐进式迁移的优缺点和注意事项

4.1 优点

  • 业务零中断:整个迁移过程,老服务一直跑,用户完全感知不到,也不会影响订单、登录等核心功能
  • 风险极低:小步迭代,每次只改少量代码,就算出错,也能快速回滚到上一个稳定状态
  • 团队易上手:可以边做边学NestJS,不用一次性啃完所有文档,学习曲线更平缓
  • 架构统一:最终项目会变成NestJS的标准架构,后续维护、扩展、新人上手都轻松很多

4.2 缺点和注意事项

缺点也很明显:在混合模式的早期,你要同时维护Express和Nest的两套配置,代码里会有一些冗余的部分,比如中间件要两边都配,不过这个阶段不会太长,一旦迁移完成,就可以删掉所有Express的代码。

注意事项一定要记牢:

  1. 路径隔离:老路由和新路由的路径绝对不能冲突,比如老的用/api/old,新的用/api/new,之后再逐步把老路径替换成新的,或者做301重定向
  2. 测试优先:每一步改动都要做测试,尤其是接口参数、返回值要和老接口完全一致,避免用户发现变化
  3. 兼容版本:用@nestjs/platform-express的时候,要注意和你现在用的Express版本兼容,比如Express 4.x对应的@nestjs/platform-express版本是9.x,别装错
  4. 灰度验证:迁移过程中可以做灰度,比如先让10%的流量走新的Nest路由,观察有没有问题,没问题再慢慢提升比例
  5. 及时清理:等所有模块都迁完了,要删掉老Express的代码,不然项目会越来越乱,后续维护成本反而升高

五、迁移后的总结

渐进式迁移的核心就是“稳”——不用追求一步到位的大改,先从最外层的路由入手,让老项目和新NestJS平稳共存,再逐步把业务逻辑抽成Nest的服务,最后整个项目换成NestJS的标准架构。这个方法适合几乎所有规模的Express项目,不管是几人的小项目,还是几十人的大团队,都能用上。

最终你会发现,原来怕“伤筋动骨”的大重构,其实拆成了一步一步的小事,只要每一步都验证没问题,就能顺利完成迁移,还能让项目的维护成本大幅下降,团队协作也更顺畅,后续不管加功能还是改需求,都能快很多。