Node.js 做服务端开发,绕不开的一个问题就是框架选型。我最早写 Node 后端的时候,用的是原生 http 模块,几十行代码才能处理一个简单的路由和请求体解析,后来接触到 Express,感觉像打开了新世界的大门。再后来 Koa2 出来了,Nest.js 也慢慢火了起来,团队里每次开新项目,总有人问“这次用哪个框架”。这个问题看起来简单,但真要回答清楚,得从架构理念、开发体验、性能表现、生态配套、团队协作等多个维度去拆。我在实际项目里用 Express 做过中小型 API 网关,用 Koa2 搭过 BFF 层,也用 Nest.js 写过完整的企业级微服务,踩过的坑不算少。这篇文章就把我对这三个框架的理解、实操经验和选型逻辑完整地梳理一遍,不管你是刚接触 Node.js 服务端的新手,还是正在做技术选型的团队负责人,应该都能从中找到有用的参考。
1. 三个框架的架构理念与核心差异
1.1 Express:中间件线性模型的经典代表
Express 的核心设计哲学可以用一句话概括:一切皆中间件。一个请求进来,依次经过注册的中间件函数,每个中间件可以选择处理请求、修改请求对象、调用下一个中间件,或者直接返回响应。这种线性模型非常直观,你几乎不需要理解什么额外的概念就能上手。
我刚开始用 Express 的时候,最直观的感受就是“自由”。路由怎么写、中间件怎么组织、错误怎么处理,全凭自己安排。比如下面这段代码,就是一个最典型的 Express 应用骨架:
const express = require('express'); const app = express(); app.use(express.json()); app.get('/users/:id', async (req, res, next) => { try { const user = await getUserById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (err) { next(err); } }); app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Internal Server Error' }); }); app.listen(3000);这段代码里,app.use(express.json())是中间件,路由处理函数是中间件,错误处理也是中间件。整个请求处理流程就是一条中间件链,谁先注册谁先执行。这种模型的优势在于学习成本极低,文档丰富,社区庞大,几乎你遇到的任何问题都能在网上找到答案。
但线性模型也有它的问题。当项目规模变大,中间件数量膨胀到几十个甚至上百个的时候,执行顺序就变得非常难以追踪。我曾经维护过一个 Express 项目,光app.use就写了四十多行,排查一个请求为什么没有走到预期的中间件,花了整整一个下午。另外,Express 对异步错误的处理一直是个痛点,在 Express 4.x 里,async 函数抛出的错误不会被自动捕获,必须手动 try-catch 然后调用 next(err),稍不注意就会导致请求挂起。
1.2 Koa2:洋葱模型与 async/await 的优雅结合
Koa2 是 Express 原班人马打造的新一代框架,它的核心改进有两个:一是采用了洋葱模型的中间件执行机制,二是原生支持 async/await。洋葱模型的意思是,中间件的执行顺序像洋葱一样层层深入,然后再层层返回。一个请求先经过最外层中间件的前半部分,再进入下一层,到达最内层处理完业务逻辑后,再依次经过各层中间件的后半部分。
这个模型最典型的应用场景就是日志和耗时统计。在 Express 里,如果你想记录一个请求的总耗时,需要在中间件里记录开始时间,然后监听 response 的 finish 事件。但在 Koa2 里,你可以直接在中间件里写:
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); app.use(async (ctx) => { ctx.body = { message: 'Hello Koa' }; }); app.listen(3000);await next()之后的代码会在所有后续中间件执行完毕后才执行,这就是洋葱模型的核心。这种设计让 Koa2 的中间件逻辑非常清晰,特别适合做请求前后的统一处理,比如响应头注入、性能监控、事务管理等。
Koa2 的另一个特点是极简。它本身不包含路由、静态文件服务、请求体解析等功能,这些都需要通过第三方中间件来实现。这种设计的好处是框架本身非常轻量,你可以完全按需组装;坏处是新手可能会觉得“怎么什么都要自己装”。我个人的经验是,Koa2 适合对 HTTP 协议有一定理解、喜欢自己掌控技术栈的开发者,不太适合完全零基础的新手。
1.3 Nest.js:企业级架构与 TypeScript 的深度融合
Nest.js 和前两个框架的定位完全不同。它不是一个轻量级的 HTTP 工具库,而是一个完整的应用框架,灵感来源于 Angular。Nest.js 的核心概念包括模块(Module)、控制器(Controller)、服务(Service)、依赖注入(DI)、装饰器(Decorator)等,这些概念对于写过 Angular 或者 Spring Boot 的开发者来说会非常熟悉。
Nest.js 默认使用 TypeScript,并且强推依赖注入和分层架构。一个典型的 Nest.js 控制器长这样:
import { Controller, Get, Param, NotFoundException } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get(':id') async findOne(@Param('id') id: string) { const user = await this.usersService.findById(id); if (!user) { throw new NotFoundException('User not found'); } return user; } }这段代码里,@Controller和@Get是装饰器,UsersService通过构造函数注入。Nest.js 的依赖注入容器会自动管理服务的生命周期和依赖关系,你不需要手动 new 一个 Service 出来。这种设计在大型项目中优势非常明显:代码结构统一、职责边界清晰、可测试性强、模块之间解耦彻底。
但 Nest.js 的学习曲线也是三个框架里最陡的。你需要理解模块系统、依赖注入、装饰器元数据、管道、守卫、拦截器、过滤器等一系列概念。我见过不少开发者第一次接触 Nest.js 时被这些概念劝退,觉得“写个接口怎么这么复杂”。但一旦跨过这个门槛,在团队协作和长期维护上带来的收益是巨大的。
1.4 三者核心差异对照
| 维度 | Express | Koa2 | Nest.js |
|---|---|---|---|
| 中间件模型 | 线性模型 | 洋葱模型 | 模块化+拦截器 |
| 异步支持 | 回调/Promise,需手动处理错误 | 原生 async/await | 原生 async/await |
| 语言 | JavaScript | JavaScript | TypeScript(默认) |
| 架构约束 | 无 | 无 | 强约束(模块/DI/分层) |
| 内置功能 | 路由、静态文件等 | 几乎无 | 路由、DI、验证、Swagger 等 |
| 学习曲线 | 低 | 中 | 高 |
| 适合场景 | 中小型项目、快速原型 | 中间层、BFF、轻量 API | 企业级应用、微服务、大型团队 |
这张表是我自己在选型时经常参考的一个总结。但光看这张表还不够,每个框架在实际使用中都有很多细节需要展开说。
2. 核心细节解析与实操要点
2.1 Express 的中间件顺序陷阱与错误处理
Express 最容易被忽视的问题就是中间件顺序。因为中间件是按注册顺序执行的,所以顺序错了,功能就会出问题。我踩过的一个典型坑是:把错误处理中间件注册在了路由之前,结果路由里抛出的错误根本不会走到错误处理中间件。错误处理中间件必须注册在所有路由之后,而且必须是四个参数(err, req, res, next),少一个参数 Express 就会把它当成普通中间件。
另一个常见问题是express.json()和express.urlencoded()的注册位置。如果你在解析请求体的中间件之前就注册了路由,那么路由处理函数里拿到的req.body就是 undefined。这个坑我在刚学 Express 的时候踩过不止一次。
关于异步错误处理,Express 5.x 已经支持自动捕获 async 函数抛出的错误,但如果你还在用 4.x,就必须手动处理。我的做法是封装一个asyncHandler高阶函数:
const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; app.get('/users/:id', asyncHandler(async (req, res) => { const user = await getUserById(req.params.id); res.json(user); }));这样每个异步路由都用asyncHandler包一层,就不用到处写 try-catch 了。这个技巧在实际项目中非常实用,建议每个用 Express 4.x 的团队都把它作为标准写法。
注意:Express 的中间件是顺序敏感的,错误处理中间件必须放在最后,请求体解析中间件必须放在路由之前。这两个顺序问题是最常见的 Express 踩坑点。
2.2 Koa2 的 ctx 对象与中间件组合
Koa2 的ctx对象是对原生req和res的封装,它把请求和响应的常用操作都挂载到了一个对象上。比如ctx.method、ctx.url、ctx.request.body、ctx.body、ctx.status等。这种设计比 Express 的req和res分离要更符合直觉,写起来也更简洁。
Koa2 的中间件组合是通过koa-compose实现的,这也是洋葱模型的技术基础。每个中间件都是一个 async 函数,接收(ctx, next)两个参数。next()返回一个 Promise,await next()会暂停当前中间件的执行,把控制权交给下一个中间件,等下一个中间件执行完后再回来继续执行。
这个机制在实现一些横切关注点时特别有用。比如实现一个简单的请求耗时统计和响应日志:
app.use(async (ctx, next) => { const start = Date.now(); try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message }; ctx.app.emit('error', err, ctx); } finally { const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`); } });这段代码里,finally块中的日志会在所有后续中间件执行完毕后执行,不管是否发生错误。这种写法在 Express 里需要监听 response 事件才能实现,Koa2 里就自然多了。
Koa2 的路由通常用koa-router或者@koa/router。需要注意的是,Koa2 本身不解析请求体,需要用koa-bodyparser中间件。静态文件服务用koa-static,跨域用@koa/cors。这些中间件都需要自己安装和注册,虽然多了一步,但也让你对项目依赖一目了然。
2.3 Nest.js 的模块系统与依赖注入
Nest.js 的模块系统是整个框架的骨架。每个应用至少有一个根模块AppModule,其他功能模块通过imports注册到根模块中。模块内部包含控制器和服务,控制器负责处理 HTTP 请求,服务负责业务逻辑。这种分层设计让代码的职责非常清晰。
依赖注入是 Nest.js 最核心的机制之一。你只需要在构造函数里声明依赖,Nest.js 的 IoC 容器就会自动帮你实例化并注入。比如:
@Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {} async findById(id: string): Promise<User | null> { return this.usersRepository.findOne({ where: { id } }); } }这里@InjectRepository(User)是 TypeORM 提供的装饰器,Nest.js 会自动把对应的 Repository 实例注入进来。这种设计的好处是,Service 不关心 Repository 是怎么创建的,只关心它的接口,非常利于单元测试和模块替换。
Nest.js 还提供了管道(Pipe)、守卫(Guard)、拦截器(Interceptor)、过滤器(Filter)等横切关注点机制。管道用于请求参数的验证和转换,守卫用于权限控制,拦截器用于请求前后的统一处理,过滤器用于异常处理。这些机制的组合使用,可以让代码的横切逻辑非常干净地抽离出来。
2.4 三个框架的 TypeScript 支持对比
Express 和 Koa2 都可以用 TypeScript 写,但它们的类型定义主要依赖@types/express和@types/koa这些社区维护的类型包。类型覆盖度还算不错,但中间件的类型推断有时候不够精确,需要手动标注类型的地方比较多。
Nest.js 则是原生 TypeScript 框架,类型系统是框架设计的一部分。装饰器、依赖注入、模块系统都深度依赖 TypeScript 的元数据反射能力。用 Nest.js 写代码,类型提示和自动补全的体验是最好的,几乎不需要手动标注类型,IDE 就能给出准确的提示。
不过 Nest.js 对 TypeScript 的强依赖也意味着,如果你团队里有人不熟悉 TypeScript,上手成本会比较高。我建议如果决定用 Nest.js,团队最好先统一学习一下 TypeScript 的基础知识,特别是装饰器和泛型,否则会在开发过程中遇到很多困惑。
3. 实操过程与核心环节实现
3.1 从零搭建一个 Express API 服务
先来看 Express 的完整搭建过程。假设我们要做一个用户管理的 API,包含列表查询、详情查询、创建用户三个接口。
第一步是初始化项目并安装依赖:
mkdir express-demo && cd express-demo npm init -y npm install express npm install -D nodemon第二步是创建入口文件app.js,把中间件、路由、错误处理都组织好:
const express = require('express'); const app = express(); // 请求体解析 app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 请求日志 app.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); next(); }); // 模拟数据 const users = [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' }, ]; // 路由 app.get('/api/users', (req, res) => { res.json({ data: users }); }); app.get('/api/users/:id', (req, res) => { const user = users.find(u => u.id === req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ data: user }); }); app.post('/api/users', (req, res) => { const { name, email } = req.body; if (!name || !email) { return res.status(400).json({ error: 'Name and email are required' }); } const newUser = { id: String(users.length + 1), name, email }; users.push(newUser); res.status(201).json({ data: newUser }); }); // 404 处理 app.use((req, res) => { res.status(404).json({ error: 'Not Found' }); }); // 错误处理 app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Internal Server Error' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });这个结构在 Express 项目里非常典型。路由直接写在入口文件里,对于小型项目来说够用,但如果接口多了,建议按功能拆分成多个路由文件,用express.Router()来组织。
第三步是在package.json里配置启动脚本:
{ "scripts": { "start": "node app.js", "dev": "nodemon app.js" } }nodemon会在文件变化时自动重启服务,开发阶段非常方便。实测下来,Express 项目从零到能跑起来,熟练的话十分钟就够了,这也是它最大的优势之一。
3.2 Koa2 项目的分层组织与中间件选型
Koa2 的搭建过程稍微多几步,因为需要自己选装中间件。同样的用户管理 API,用 Koa2 来实现:
mkdir koa-demo && cd koa-demo npm init -y npm install koa @koa/router koa-bodyparser @koa/cors npm install -D nodemon入口文件app.js的结构:
const Koa = require('koa'); const Router = require('@koa/router'); const bodyParser = require('koa-bodyparser'); const cors = require('@koa/cors'); const app = new Koa(); const router = new Router({ prefix: '/api' }); // 全局错误处理 app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message }; ctx.app.emit('error', err, ctx); } }); // 请求日志 app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`); }); app.use(cors()); app.use(bodyParser()); // 模拟数据 const users = [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' }, ]; router.get('/users', (ctx) => { ctx.body = { data: users }; }); router.get('/users/:id', (ctx) => { const user = users.find(u => u.id === ctx.params.id); if (!user) { ctx.status = 404; ctx.body = { error: 'User not found' }; return; } ctx.body = { data: user }; }); router.post('/users', (ctx) => { const { name, email } = ctx.request.body; if (!name || !email) { ctx.status = 400; ctx.body = { error: 'Name and email are required' }; return; } const newUser = { id: String(users.length + 1), name, email }; users.push(newUser); ctx.status = 201; ctx.body = { data: newUser }; }); app.use(router.routes()); app.use(router.allowedMethods()); app.on('error', (err) => { console.error('Server error:', err); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });对比 Express 的版本,Koa2 的代码有几个明显不同:错误处理用 try-catch 包在中间件里,而不是单独的错误处理中间件;路由用@koa/router的实例来管理;响应通过ctx.body和ctx.status设置。整体代码量差不多,但 Koa2 的中间件执行顺序更符合直觉,特别是日志中间件里await next()前后的代码分界非常清晰。
Koa2 项目在实际使用中,我建议把路由、控制器、服务分层组织。路由只负责定义 URL 和 HTTP 方法,控制器负责处理请求和响应,服务负责业务逻辑。这样即使项目变大,代码也不会乱。
3.3 Nest.js 企业级项目的模块化实现
Nest.js 的搭建过程是最复杂的,但它的 CLI 工具可以帮我们省很多事。先安装 CLI:
npm install -g @nestjs/cli nest new nest-demoCLI 会交互式地问你用什么包管理器,选 npm 或 yarn 都行。创建完成后,项目结构已经帮你组织好了:
src/ app.controller.ts app.service.ts app.module.ts main.ts接下来生成用户模块:
nest generate module users nest generate controller users nest generate service users这三个命令会分别创建users.module.ts、users.controller.ts、users.service.ts,并且自动把 UsersModule 注册到 AppModule 的 imports 里。这种代码生成能力在大型项目中非常实用,能保证团队所有人的代码结构一致。
用户服务的实现:
// users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; export interface User { id: string; name: string; email: string; } @Injectable() export class UsersService { private users: User[] = [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' }, ]; findAll(): User[] { return this.users; } findById(id: string): User { const user = this.users.find(u => u.id === id); if (!user) { throw new NotFoundException('User not found'); } return user; } create(name: string, email: string): User { const newUser: User = { id: String(this.users.length + 1), name, email, }; this.users.push(newUser); return newUser; } }控制器的实现:
// users.controller.ts import { Controller, Get, Post, Param, Body, HttpCode } from '@nestjs/common'; import { UsersService } from './users.service'; class CreateUserDto { name: string; email: string; } @Controller('api/users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() findAll() { return { data: this.usersService.findAll() }; } @Get(':id') findOne(@Param('id') id: string) { return { data: this.usersService.findById(id) }; } @Post() @HttpCode(201) create(@Body() createUserDto: CreateUserDto) { const { name, email } = createUserDto; return { data: this.usersService.create(name, email) }; } }模块定义:
// users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], }) export class UsersModule {}最后在main.ts里启动应用:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); console.log('Server running on port 3000'); } bootstrap();Nest.js 的代码量明显比前两个框架多,但结构非常清晰。控制器只负责路由和参数提取,服务只负责业务逻辑,模块负责组装。这种分层在项目变大后优势会越来越明显。另外,Nest.js 内置了参数验证管道,配合class-validator可以自动验证请求参数,不需要手动写 if-else 判断。
3.4 性能压测对比与参数调优
三个框架的性能差异是很多人关心的问题。我用autocannon做过一轮简单的压测,测试环境是本地开发机,Node.js 18 LTS,每个框架都跑一个返回 JSON 的简单接口,并发连接数 100,持续 10 秒。结果如下:
| 框架 | 每秒请求数(RPS) | 平均延迟(ms) | 吞吐量(MB/s) |
|---|---|---|---|
| Express | 约 12000 | 8.2 | 2.1 |
| Koa2 | 约 13500 | 7.1 | 2.4 |
| Nest.js | 约 11000 | 9.5 | 1.9 |
这个数据只是参考,实际性能受业务逻辑、数据库查询、中间件数量影响很大。从结果看,Koa2 因为中间件机制更轻量,性能略好于 Express;Nest.js 因为多了依赖注入和装饰器元数据的开销,性能稍低,但差距在可接受范围内。
真正影响性能的往往不是框架本身,而是数据库查询、序列化、日志、中间件数量这些因素。我在实际项目中做过优化,把 Express 项目里的一个同步日志中间件改成异步写入后,RPS 提升了将近 20%。所以选型时不用太纠结框架的基准性能,更应该关注开发效率和维护成本。
提示:如果确实对性能有极致要求,可以考虑在框架前面加一层反向代理做负载均衡,或者用 Node.js 的 cluster 模块充分利用多核 CPU。框架层面的性能差异在真实业务场景中通常不是瓶颈。
4. 常见问题与排查技巧实录
4.1 Express 常见问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| req.body 为 undefined | 请求体解析中间件未注册或注册顺序错误 | 确保express.json()在路由之前注册 |
| 错误处理中间件不生效 | 注册位置在路由之前,或参数不是四个 | 移到所有路由之后,确保参数为(err, req, res, next) |
| 异步路由错误导致请求挂起 | Express 4.x 不自动捕获 async 错误 | 用asyncHandler包装异步路由 |
| 跨域请求失败 | 未配置 CORS 中间件 | 安装cors并在路由之前注册 |
| 路由匹配不到 | 路由顺序问题或路径拼写错误 | 检查路由注册顺序,用express.Router()拆分 |
Express 的这些问题我基本都踩过一遍。最让人头疼的是异步错误导致请求挂起,因为服务不会崩溃,只是那个请求一直没有响应,排查起来很费时间。后来我养成了习惯,所有异步路由一律用asyncHandler包装,再也没遇到过这个问题。
4.2 Koa2 常见问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| ctx.request.body 为 undefined | 未注册koa-bodyparser | 安装并在路由之前注册 |
| 路由 404 | 未注册router.routes()或 prefix 配置错误 | 检查app.use(router.routes())和 prefix |
| 中间件不执行 | 忘记调用await next() | 确保每个中间件都调用了next() |
| 错误未被捕获 | 没有全局错误处理中间件 | 在最外层中间件用 try-catch 捕获 |
| 静态文件 404 | 未注册koa-static或路径配置错误 | 检查静态文件中间件的注册路径 |
Koa2 最常见的问题就是忘记await next()。因为 Koa2 的中间件必须显式调用next()才会继续往下执行,如果忘了写,请求就会停在那里。这个坑我在刚用 Koa2 的时候踩过,排查了半天才发现是少写了一个await next()。
4.3 Nest.js 常见问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 依赖注入失败 | Service 未在 Module 的 providers 中注册 | 检查 Module 的 providers 和 exports |
| 路由 404 | Controller 未在 Module 的 controllers 中注册 | 检查 Module 的 controllers 配置 |
| 参数验证不生效 | 未使用 ValidationPipe 或 DTO 未加装饰器 | 在 main.ts 中全局注册 ValidationPipe |
| 循环依赖报错 | 两个模块互相 imports | 使用forwardRef()解决循环依赖 |
| TypeORM 连接失败 | 数据库配置错误或实体未注册 | 检查 TypeOrmModule 配置和实体注册 |
Nest.js 的依赖注入问题是最常见的,特别是新手容易忘记在 Module 里注册 Service。我建议每次新建 Service 后,先检查 Module 的 providers 数组里有没有它,这个习惯能省很多排查时间。
4.4 框架选型的实操建议
说了这么多技术细节,最后落到选型上,我的建议是这样的:
如果是个人项目、快速原型、或者团队规模小且追求开发速度,Express 依然是最稳妥的选择。它的生态最成熟,遇到问题最容易找到解决方案,招人也最容易。
如果项目需要处理大量请求前后的统一逻辑,比如 BFF 层、API 网关、需要精细控制中间件执行顺序的场景,Koa2 的洋葱模型会让你写得更舒服。但前提是团队对 HTTP 协议和异步编程有一定理解。
如果是企业级应用、微服务架构、团队规模较大且需要长期维护,Nest.js 的架构约束和 TypeScript 支持会带来巨大的长期收益。虽然前期学习成本高,但项目越大,Nest.js 的优势越明显。
我个人的经验是,不要为了用新框架而用新框架。选型的核心依据是团队的技术储备、项目的生命周期、以及维护成本。一个用 Express 写得清清楚楚的项目,远比一个用 Nest.js 写得乱七八糟的项目要好维护得多。
另外补充一个实际工作中的小技巧:如果团队已经在用某个框架,除非有非常明确的理由,否则不要轻易换框架。换框架的迁移成本往往被低估,而收益往往没有想象中那么大。我见过一个团队从 Express 迁移到 Nest.js,花了三个月才把核心业务迁移完,期间新功能开发几乎停滞。这个代价在选型时一定要考虑进去。