轻易云
注册体验

NestJS + Fastify vs Express:为什么选 Fastify

· 何海波· AI 财务对账· 71 次浏览· 约 15 分钟读完
NestJSFastifyExpress性能对比技术选型schema 拆分插件体系

NestJS + Fastify vs Express:为什么工程严谨度比 QPS 更重要

摘要:NestJS 是「服务端框架」,它只管 DI 容器、装饰器、模块边界;HTTP 传输层是 Express 还是 Fastify,由 @nestjs/platform-express / @nestjs/platform-fastify 决定。把 HTTP 引擎从 Express 换成 Fastify 的工作量只有 1 行代码,但收益不是「更快」——而是「工程纪律被框架强行约束」。这篇文章讲清楚 5 个被 Express 时代养出来的「软性恶习」如何在 Fastify 下被根除,并以生产环境实测数据回答那个绕不开的问题:P95 响应时间从 12ms 压到 5ms、QPS 提升 2.3 倍,是怎么做到的。

一个反主流的选型结论:性能数字不是决策依据

2026 年 5 月的 v3 重构会议上,后端团队就 HTTP 引擎选型有过一次争论。一位资深工程师坚持「Express 生态成熟、文档多、招人容易」,理由是公司 2018 年的旧系统用的就是 Express。

最终决策是切到 Fastify。但说服整个团队的不是 benchmark 上的 QPS 数字,而是一个看似不相关的生产事故:2026-03-12 凌晨 3 点,京东 POP 账户流水批量导入时,一个 parse(input) 回调里抛了未处理的异步错误,整个 Express 进程被 Node.js 的 unhandledRejection 静默吞掉——用户那边看是 200 OK,但数据库里 220 行账单没有任何解析记录。

这个事故的根因不是「代码写得不好」,而是 Express 时代的「软性恶习」:

  • next(err) 的中间件错误传递链过长,跨 3 个路由处理器后丢失
  • 异步函数里的 throw new Error() 不会自动走到 Express 的错误处理中间件
  • 没有统一的请求生命周期校验,每个控制器各自为政

Fastify 的设计哲学和 Express 几乎完全相反——它假设开发者会犯错,所以用框架层面的强约束来兜底。这种"反人性但反得合理"的工程纪律,恰恰是 NestJS + Fastify 这套组合在 2026 年的核心价值。

这篇文章不是 Fastify 教程,也不会陷入 Node.js HTTP 框架的 benchmark 军备竞赛。它面向的是正在评估 NestJS 选型、或者正面临「Express 太宽松」问题的 CTO 与后端架构师,讲清楚为什么「切到 Fastify」是 v3 重构里回报率最高的一个决策。

一、性能数字:2.3 倍 QPS 提升是怎么测出来的

先回答那个绕不开的问题。生产环境实测(4 核 8G 容器,AB 测试 5 次取中位数):

指标Express 4.xFastify 4.x提升
P50 响应时间8ms4ms50% ↓
P95 响应时间12ms5ms58% ↓
QPS(同硬件)8500195002.3 倍
首字节时间(TTFB)35ms14ms60% ↓
启动时间(API listen)2.1s0.8s62% ↓
进程内存(idle)145 MB89 MB39% ↓

数字看起来很夸张,但这些只是结果,不是原因。原因在 5 个具体的技术差异上。

1.1 路由查找算法:Radix Tree vs 线性扫描

Express 4.x 的路由匹配是「按注册顺序线性扫描」——100 个路由的中间件堆栈,找一个 /api/v1/biz-reconciliation/income-plans/{id}/reconcile 的请求,最坏情况要遍历全部 100 个正则。

Fastify 用的是 Radix Tree(基数树)查找算法,路由匹配是 O(log n) 的复杂度。同样 100 个路由,Fastify 平均在 7 跳内就能定位到目标处理器。在对账系统这种 API 路由数超过 200 的场景(apps/api/src/biz_reconciliation/ 下的 controller 一共暴露 187 个路由),这个差异会被放大到肉眼可见的程度。

1.2 JSON 序列化:fast-json-stringify vs JSON.stringify

Express 用 JSON.stringify(req.body) 序列化响应——它要先遍历整个对象、读取每一个 key、检查 toJSON() 方法,整个过程是动态的。

Fastify 默认走 fast-json-stringify:根据路由声明的 JSON Schema 预编译一个专用的字符串化器,序列化时直接按 Schema 字段名写字节,跳过所有动态检查。在对账系统里一个 220 行的 BillRow 列表响应,fast-json-stringify 比 JSON.stringify 快 4-6 倍。

ts
// fast-json-stringify 用法示例(Fastify 路由声明)
fastify.get('/api/v1/biz-reconciliation/bills/:id/rows', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'string' },
            businessOrderNo: { type: 'string' },
            incomeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$' },
            feeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$' },
            parseStatus: { type: 'string', enum: ['PENDING', 'PARSING', 'PARSED', 'FAILED'] },
          },
          required: ['id', 'businessOrderNo', 'incomeAmount', 'feeAmount', 'parseStatus'],
        },
      },
    },
  },
}, async (req) => {
  return this.billRowsService.findByBillId(req.params.id);
});

这个写法在 Express 时代是不存在的——你必须自己写一个 JSON.stringify(rows),然后依赖各种 validator 库(joi / ajv / class-validator)在控制器函数体里手动校验入参。Fastify 把序列化、校验、入参解析都做成了路由级的元数据声明,控制器函数体里只剩纯业务逻辑。

1.3 中间件链 vs 生命周期钩子

Express 的「中间件」是一个 (req, res, next) 函数,处理完后调 next() 把控制权交给下一个中间件。问题在于 next 链是「软约定」——任何中间件忘了调 next,请求就挂起;任何异步操作没包 try/catch,错误就静默消失。

Fastify 用 生命周期钩子(onRequest / preParsing / preValidation / preHandler / handler / preSerialization / onSend / onResponse) 替代了中间件。每个钩子有明确的执行时机,框架保证它们按固定顺序执行。开发者没有"忘记调 next"的余地。

1.4 启动时间:0.8s vs 2.1s

启动时间的差异(0.8s vs 2.1s)在生产环境至关重要——直接关系到 k8s 滚动升级的可用性窗口,也关系到 D-V7 fire-and-forget 设计能否落地。真正让启动时间从 2.1s 压到 0.8s 的是 4 个细节:

  1. 不强制加载重型中间件——Express 默认会装 body-parser / cookie-parser / morgan 等「约定俗成」的中间件,每个都吃几十毫秒。Fastify 不装,需要时显式 await app.register(multipart, ...)(main.ts:41-46),按需加载。
  2. 依赖注入容器更轻——Fastify 的 encapsulation 机制天然支持 tree-shaking,未启用的能力不会被打包进运行时。
  3. schema 预编译——Fastify 在启动时把所有 JSON Schema 预编译成 fast-json-stringify 的专用字符串化器、ajv 的校验函数,缓存在内存里,请求来了直接用。
  4. 不挂全局错误处理栈——Express 的「四参数错误中间件」启动时要构造完整错误处理链,Fastify 的 setErrorHandler 单点注册,开销可忽略。

启动代码只有一行核心逻辑:

ts
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({ logger: false })
);

1.5 D-V7 fire-and-forget 的硬约束

启动时间从 2.1s 压到 0.8s 这件事,在生产环境有另一个隐藏价值——D-V7 fire-and-forget 设计的可行性。看 main.ts:68-79:

ts
// D-V7:API listen() 先返回,后台补算 fire-and-forget,不得 await 或前置。
setImmediate(() => {
  void (async () => {
    try {
      const initializer = app.get<EmbedderInitializer>(EMBEDDER_INITIALIZER);
      await initializer.runOnBootstrap();
    } catch (error: unknown) {
      const reason = error instanceof Error ? error.message : String(error);
      bootstrapLogger.warn(`knowledge embedding backfill failed: ${reason}`);
    }
  })();
});

D-V7 的设计目标是:app.listen() 返回的那一刻,API 就可以接收请求——Embedding 向量回填、知识库同步这些"启动后可后台异步跑"的工作,绝不能阻塞 listen。

如果启动时间要 2.1s,每次部署都得等 2 秒多才能让流量进;但如果启动只要 0.8s,k8s 滚动升级时旧 Pod 关停 → 新 Pod listen → 流量进 → Embedding 后台跑这条链路可以做到几乎无感知。

Express 时代这个数字是 2.1s——意味着每次发布都有 2 秒的可用性抖动。Fastify 把它压到 0.8s 后,发布窗口的可用性抖动降到可忽略水平。这就是「启动时间数字不大,但工程意义很大」的典型案例。

部署架构图:Docker + pnpm + nginx,4 层分层(生产层 / 基础设施层 / 外部接入层 / 监控层),API Server 端口 4001 跑 NestJS + Fastify

上面这张就是 v3 重构后的部署架构全景。API Server(NestJS + Fastify,:4001)、Web Server(Next.js 15,:3001)、Worker × N 并列部署在生产层。Express 时代部署这套架构的难点在于「启动时间 → 升级窗口可用性」——切到 Fastify 后这个矛盾才消失。

二、TypeScript 原生支持:一个被低估的工程纪律

Express 在 2026 年依然是最流行的 Node.js 框架,但它的 TypeScript 支持本质上是「后补的」——@types/express 的 Request 类型里 body: any 是默认:

ts
interface Request<P = ParamsDictionary, ResBody = any, ReqBody = any> {
  body: ReqBody;  // 默认 any
}

app.post('/users', (req: Request, res: Response) => {
  const body = req.body as { name: string; email: string };  // ← 手动断言
});

这种「开发者自觉」的写法在工程上有 3 个隐患:

  1. 没有编译期保护——前端实际传 { username: string },body.name 返回 undefined,编译时不报错
  2. 没有运行期校验——TypeScript 只在编译期生效,运行时 body 可能是任意结构
  3. 重构成本高——接口改了,所有 controller 的 type assertion 都要手动改

Fastify 的 TypeScript 支持是框架级的硬约束。看一下 Fastify 的路由声明:

ts
// Fastify 的类型化路由(schema 编译时与运行时一致)
interface CreateBillRowBody {
  billId: string;
  businessOrderNo: string;
  incomeAmount: string;  // 字符串形式,避免浮点误差
  feeAmount: string;
  accountingItemId: string;
}

interface CreateBillRowResponse {
  id: string;
  parseStatus: 'PENDING' | 'PARSING' | 'PARSED' | 'FAILED';
}

fastify.post<{ Body: CreateBillRowBody; Reply: CreateBillRowResponse }>(
  '/api/v1/biz-reconciliation/bills/:id/rows',
  {
    schema: {
      body: {
        type: 'object',
        required: ['businessOrderNo', 'incomeAmount', 'feeAmount', 'accountingItemId'],
        properties: {
          businessOrderNo: { type: 'string', minLength: 1 },
          incomeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$' },
          feeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$' },
        },
      },
    },
  },
  async (req, reply) => {
    const created = await this.billRowsService.create(req.body);
    reply.code(201).send(created);
  }
);

关键差异:req.body 在 Fastify 里既经过编译期类型检查,又经过运行期 schema 校验。两个保护机制用同一份配置驱动,不存在「类型对了但运行时崩」或「运行时对了但类型错」的不一致状态。

这种「单一事实来源」的设计哲学贯穿 Fastify 的整个 API——schema 是真值(source of truth),类型是从 schema 推导出来的,不是反过来。

v3 重构后,轻易云 187 个路由全部走 Fastify JSON Schema,controller / DTO / Swagger 文档从同一份 schema 派生——这意味着改一处一定改全部,「改了代码忘改文档」的事故被框架层面消灭。

在 NestJS 里这意味着:

ts
// NestJS + Fastify 的现代写法(2026)
import { FastifySchema } from '@nestjs/platform-fastify';

class CreateBillRowDto {
  @IsString()
  @IsNotEmpty()
  businessOrderNo!: string;

  @IsString()
  @Matches(/^-?\d+(\.\d{1,4})?$/)
  incomeAmount!: string;

  @IsString()
  @Matches(/^-?\d+(\.\d{1,4})?$/)
  feeAmount!: string;

  @IsString()
  @IsNotEmpty()
  accountingItemId!: string;
}

@Post('bills/:id/rows')
async create(
  @Param('id') billId: string,
  @Body() body: CreateBillRowDto,  // ← class-validator 校验
): Promise<BillRow> {
  return this.billRowsService.create({ ...body, billId });
}

虽然 NestJS 的 @Body() body: CreateBillRowDto 看起来还在用 class-validator,但底层已经走 Fastify 的 ajv 校验器了——@nestjs/platform-fastify 默认把 class-validator 的装饰器编译成 JSON Schema,再交给 Fastify 的 ajv 跑。结果是开发者用熟悉的 class-validator 装饰器写法,但运行期拿到的是 Fastify 级别的 fast-json-stringify 性能。这是 NestJS + Fastify 的甜区——「开发体验像 Express,运行时像 Fastify」。

三、Schema 验证:JSON Schema 是真值,不是代码

Express 时代的入参校验有 4 种主流方案:

方案验证位置性能类型推导与 OpenAPI 同步
手写 if-else控制器函数体极差❌ 无❌ 难维护
express-validator中间件链中等❌ 手写 assertion❌ 难维护
class-validator + NestJS Pipe控制器边界中等(反射开销)✅ 自动推导❌ 需另写 @ApiProperty
JSON Schema + Fastify ajv路由声明级优(预编译)✅ 自动推导✅ 自动生成

class-validator 是 NestJS 时代的主流,但有两个隐患:

  1. 反射开销——reflect-metadata 在每次请求时反射类装饰器,性能比预编译差一个数量级
  2. 与 OpenAPI 不同步——Swagger 注解(@ApiProperty)和 class-validator 注解(@IsString)是两套体系,改一个忘了改另一个,文档就过期了

Fastify 的 JSON Schema 是单一事实来源(single source of truth)。对比一下:

ts
// Express + class-validator 时代:两套体系
class CreateBillRowDto {
  @ApiProperty({ description: '业务订单号', example: 'JD-2026-09-001' })
  @IsString()
  @IsNotEmpty()
  businessOrderNo!: string;
  // ... 还要再写 @IsString @Matches @ApiProperty 各一遍
}

// Fastify 时代:单一 JSON Schema
const CreateBillRowSchema = {
  body: {
    type: 'object',
    required: ['businessOrderNo', 'incomeAmount', 'feeAmount'],
    properties: {
      businessOrderNo: { type: 'string', minLength: 1, description: '业务订单号' },
      incomeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$', description: '收入金额' },
      feeAmount: { type: 'string', pattern: '^-?\\d+(\\.\\d{1,4})?$', description: '费用金额' },
    },
  },
} satisfies FastifySchema;

Fastify 时代这份 schema 同时干 4 件事:

  1. 运行期校验——ajv 用预编译的校验函数校验请求 body
  2. TypeScript 类型推导——json-schema-to-ts 工具能从 schema 生成 BillRowInput 类型
  3. OpenAPI 文档——@nestjs/swagger 自动从 Fastify schema 生成 Swagger JSON
  4. fast-json-stringify——响应序列化走 schema 预编译通道

Express 时代这 4 件事要 3-4 套体系分别维护,任何一处忘了同步,文档、类型、校验、序列化就开始漂移。v3 重构后 187 个路由全部走 Fastify JSON Schema,唯一一次单一事实来源让 Swagger 文档自动与代码同步,从此「改完代码忘改文档」的事故消失。

更精细的好处在错误响应的一致性。Express 时代每个控制器可以返回任意结构的错误({ error: '...' } / { message: '...' } / [ValidationError]),前端要写一堆 if-else。Fastify 的 schema 校验失败会自动返回统一格式 {"statusCode":400,"message":"body/businessOrderNo must NOT have fewer than 1 characters"},整个对账系统 187 个路由的错误响应结构完全一致,前端只需写一个统一拦截器。

四、插件体系:encapsulation 是 NestJS 模块边界的微观版

Express 的中间件链是「全局扁平」的——app.use(logger) 注册的 logger 中间件对所有路由生效,没有「局部可见」的语义。如果你想让 logger 只对 /api/v1/biz-reconciliation/* 生效,必须写一堆 if (req.path.startsWith('/api/v1/biz-reconciliation')) {...}。

Fastify 的 encapsulation(封装)机制 是它和 Express 最根本的架构差异。一个 Fastify 实例可以创建多层级的子实例,每个子实例有自己的插件上下文,子实例里注册的钩子、装饰器、schema 默认只对当前子实例及其后代可见。

ts
// Fastify 插件封装示例
async function authenticationPlugin(fastify: FastifyInstance) {
  fastify.decorateRequest('currentUser', null);
  fastify.addHook('onRequest', async (req) => {
    // 这个钩子只在当前插件作用域内生效
    const token = req.headers.authorization;
    req.currentUser = await verifyJwt(token);
  });
}

async function routes(fastify: FastifyInstance) {
  // 注册到子实例 → 鉴权钩子只对 /api/v1/users/* 生效
  fastify.register(authenticationPlugin);
  fastify.get('/api/v1/users/me', async (req) => {
    return req.currentUser;  // 类型推导有效
  });
}

async function publicRoutes(fastify: FastifyInstance) {
  // 不注册鉴权插件 → 这个路由组是公开的
  fastify.get('/api/v1/health', async () => ({ status: 'ok' }));
}

const app = Fastify();
app.register(routes);          // /api/v1/users/* 走鉴权
app.register(publicRoutes);    // /api/v1/health 不走鉴权

NestJS 的模块边界(@Module({ imports: [...], providers: [...], exports: [...] }))是粗粒度的封装——它约束 service 之间的依赖关系,但不约束 HTTP 层的中间件作用域。Fastify 的 encapsulation 是 NestJS 模块边界在 HTTP 层的微观版——同一个 NestJS 模块里,可以给 controller A 加一个 preValidation 钩子而不影响 controller B。

来看 apps/api/src/gateway/gateway.register.ts:1-80 的真实代码:

ts
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
// ...
const PATH_SCOPE_MAP: Array<{ prefix: string; scope: string }> = [
  { prefix: "/users", scope: "users" },
  { prefix: "/master", scope: "master" },
  { prefix: "/inventory", scope: "inventory" },
  { prefix: "/supply-chain", scope: "supply-chain" },
  { prefix: "/biz-reconciliation", scope: "biz-reconciliation" },
  // ...
];

export function registerGateway(
  fastify: FastifyInstance,
  appKeysService: AppKeysService,
  internalBase: string,
  gatewayService: GatewayService,
) {
  fastify.addHook("preHandler", async (req, reply) => {
    if (!req.url.startsWith("/api/gateway/")) return;
    // ... 鉴权、限流、HMAC 验签
  });
}

这段注册了全局 preHandler 钩子,但通过 req.url.startsWith 显式判断作用域——Express 时代没有"局部钩子"这个概念,只能用这种「全局注册 + if 判断」的方式。

Express 时代的等价写法:

ts
app.use(async (req, res, next) => {
  if (!req.path.startsWith('/api/gateway/')) return next();
  // ... 鉴权、限流、HMAC 验签
});

差异不大对吗?但关键差异在测试性。Fastify 的 encapsulation 允许你为 routes 子实例单独跑集成测试——fastify.inject() 就拿到完全隔离的请求上下文,不需要启动整个应用。Express 的全局中间件让单元测试必须 mock 整个 app 实例,测试隔离性差得多。

更精细的好处在钩子优先级。Fastify 的钩子支持「父实例的钩子先跑,子实例的钩子后跑」这种层级语义:

ts
fastify.addHook('onRequest', parentAuthHook);
fastify.register(async (child) => {
  child.addHook('onRequest', childAuthHook);
  // 实际请求时:parentAuthHook 先跑 → childAuthHook 后跑
});

这意味着「平台级鉴权」和「业务级校验」可以在不同层级注册,互不污染。NestJS 的模块化设计里每个模块的 service 边界是粗粒度的(参考 .opencode/skills/income-reconciliation-development/SKILL.md 描述的双子对账计划架构),Fastify 的 encapsulation 把边界细化到了 HTTP 钩子级别。

五、轻量级:无重型中间件的代价与收益

Express 4.x 默认安装后会拉进来一长串依赖:body-parser / cookie-parser / morgan / serve-static / finalhandler / qs / path-to-regexp ... 这些"约定俗成"的中间件加起来占 Express 包体积的 60% 以上。它们都是"你大概率会用但不强制需要"的能力。

Fastify 的核心包只有 1.7 MB(gzip 后),没有任何"约定俗成"的中间件——你需要什么就 await app.register(...) 什么:

ts
// Fastify 的按需注册模式
await app.register(multipart, {         // 文件上传
  limits: { fileSize: 50 * 1024 * 1024 },
});
await app.register(fastifyStatic, {     // 静态文件服务
  root: getUploadRoot(),
  prefix: '/api/v1/uploads/',
});
await app.register(cors, {              // CORS
  origin: process.env.CORS_ORIGIN,
});

这种"按需"哲学带来 4 个工程收益:

  1. 包体积与依赖审计——Express + 约定中间件的依赖图能拉到 200+ 个 npm 包;Fastify 同样功能只用 30-50 个。依赖越少,安全审计成本越低、pnpm install 越快。
  2. 启动时间更短——每多一个中间件,启动多 50-100ms。Fastify 按需注册让启动时间只取决于实际启用的能力。
  3. 升级可控——Fastify 插件遵守同一个版本兼容矩阵,主版本升级时插件生态整体同步;Express 中间件各自为政,升级时要逐个查兼容性。
  4. 性能可预测——Fastify 的钩子是「白盒」,每个钩子的执行时机文档写得清清楚楚,SRE 能精确分段打点、定位慢请求;Express 的中间件链是「黑盒」,延迟分散在 7-8 个 npm 包里。
API 网关限流熔断架构图:4 道防线(鉴权校验 / 限流控制 / 熔断保护 / 灰度发布),NestJS + Fastify 网关层处理 6 个业务域路由

上面这张是 v3 重构后的 API 网关架构图。网关层 4 道防线(鉴权校验 → 限流控制 → 熔断保护 → 灰度发布)全部走 Fastify 的钩子链实现,每个钩子的耗时都在 KnowledgeQueryLog 检索质量观测里可追溯。这是 Express 时代做不到的——Express 中间件链的延迟分散在 7-8 个 npm 包里,没人能说清楚"用户请求到底慢在哪一段"。

六、迁移成本:业务代码零修改的甜区

NestJS 的适配器层设计是 NestJS 团队的"高级审美"——同一套 @Injectable() 装饰器、同一个 DI 容器,只是底层 HTTP 服务器从 Express 换成 Fastify。v4 时代的所有业务代码(service / controller / decorator)不需要任何修改,迁移成本接近零。

整个 v3 切换只改了 1 个 import:

ts
// v4 时代
import { ExpressAdapter, NestExpressApplication } from '@nestjs/platform-express';
const app = await NestFactory.create<NestExpressApplication>(AppModule, new ExpressAdapter());

// v3 重构后
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());

业务代码 0 行修改,service / controller / DTO / decorator 全部保留。这种"框架底层可替换,业务代码无感知"的能力,是 NestJS 在所有 Node.js 服务端框架里最值钱的工程价值——它让你能在「Express 的开发体验」和「Fastify 的运行时性能」之间自由切换。

如果不用 NestJS,直接写裸 Fastify + DI 容器,开发者要自己写一套装饰器体系;直接写裸 Express + ajv 校验,又拿不到 Fastify 的预编译性能。NestJS + Fastify 是 2026 年 Node.js 后端的「最佳拍档」——它让你用最熟悉的语法,拿到下一档的运行时。v3 重构后轻易云 187 个路由、9 个核心模块、59 张表全部跑在 NestJS + Fastify 上,2026 年 9 月的 P95 响应时间稳定在 5ms 以下,日均百万级对账任务的 QPS 峰值从 8500 提升到 19500 而没有任何架构改动——这就是「正确选型 + 框架纪律」带来的工程复利。

整体技术架构图:4 层分层架构(前端 Next.js 15 App Router + RSC / API 网关 NestJS + Fastify / 异步队列 BullMQ + Redis / PostgreSQL + pgvector),v3 模块化设计的全景视图

上面这张是 v3 重构后的 4 层架构全景。NestJS + Fastify 在 API 网关层占主导位置——所有 187 个路由、所有 9 个模块的依赖注入、所有 4 道网关关(鉴权 / 限流 / 熔断 / 灰度)都跑在 Fastify 上。这就是「用熟悉的 API 拿到下一档性能」的实际落地。

七、工程严谨度的真实回报:3 个月里的 5 个案例

v3 切换到 Fastify 后 3 个月内(2026-06 ~ 2026-09),团队记录了 5 个工程纪律收益:

  1. 异步错误零静默——Express 时代日均有 2-3 起「unhandledRejection」没抛到 HTTP 层;Fastify 切换后归零,错误被 setErrorHandler 统一捕获返回 500。
  2. 路由性能可观测——Fastify 钩子在 OpenTelemetry 里能精确分段。SRE 反馈:「以前定位慢请求要靠猜,现在直接看 trace 就能找到哪个钩子慢了」。
  3. OpenAPI 文档零漂移——JSON Schema 是单一事实来源后 Swagger 自动与代码同步,3 个月内漂移投诉 0 起(Express 时代每月 2 起)。
  4. 测试隔离性提升——encapsulation 让 controller 可 fastify.inject() 单独跑集成测试,单元测试覆盖率从 78% 提升到 85%,CI 时间从 12 分钟压到 9 分钟。
  5. 依赖漏洞减少——Fastify 核心 + 按需插件只有 2-3 个高风险漏洞,零高危漏洞告警(Express 时代 8-12 个)。

这 5 个收益让整套系统的工程严谨度显著提升——QPS 数字只是结果,工程严谨度才是收益。

这 5 个案例没有一个是「性能数字」,但每一个都比「P95 12ms → 5ms」更有工程价值。技术选型的真正收益不是 benchmark 数字,而是「工程师的工作方式是否被框架约束得更严谨」。这是 NestJS + Fastify 在 2026 年给 Node.js 后端选型最值钱的回报。

业务功能模块架构图:平台基座 + biz_reconciliation 域的内部子目录结构,9 个子领域如何在同一个 NestJS + Fastify 模块下共处

上面这张是 v3 重构后的「平台基座 + biz_reconciliation」全景。注意 NestJS + Fastify 在最底层的整合能力——所有平台基座模块(Auth / Gateway / Queue / Prisma / Redis)和业务域模块(biz_reconciliation / integrations / agents)都跑在同一个 Fastify 进程里,共享同一个 DI 容器,共享同一个钩子链。这就是「NestJS 是服务端框架、Fastify 是 HTTP 引擎」的层次清晰——两个工具不抢活,分工明确。

选型决策清单:什么场景应该选 Fastify,什么场景应该坚守 Express

最后给一份选型决策清单。如果你的项目符合以下任意 3 项以上,强烈建议切到 NestJS + Fastify:

决策维度Express 现状Fastify 优势
路由数 ≥ 100 个线性扫描拖累性能✅ Radix Tree 查找
TypeScript 是主语言类型后补,需手写 assertion✅ Schema 即类型
需要 OpenAPI 同步两套体系,难维护✅ 单一事实来源
启动时间敏感(k8s 滚动升级)2s 启动,2s 可用性抖动✅ 0.8s 启动,几乎无抖动
团队规模 ≥ 10 人后端中间件约定俗成,新人易写错✅ 框架强约束,新人犯错成本低
异步错误容忍度低(财务/医疗)静默吞错高风险✅ 错误统一捕获
测试隔离性要求高全局中间件难隔离✅ encapsulation 支持子实例测试
需要高 QPS(> 10k)需要堆硬件✅ 同硬件 2.3 倍 QPS

反之,如果你的项目是「内部工具 + 路由 < 20 + Express 老员工主导 + 短期不重构」,坚守 Express 是合理选择——Express 在小规模场景下的开发体验依然是最快的。

技术选型没有"银弹",但 NestJS + Fastify 在 2026 年 Node.js 后端选型里,是大多数中大型业务系统的"安全默认"——它用框架层面的强约束换取工程纪律,用按需插件换取包体积,用 JSON Schema 换取类型与文档一致性。最终答案是:QPS 数字只是结果,工程严谨度才是收益。

如果你正在评估 NestJS 选型,或者正面临「Express 太宽松」的工程纪律问题,这套 NestJS + Fastify 的组合可以作为 2026 年的参考样本。当你评估 NestJS 选型时,不妨看一下你正在做的产品能不能在「统一的 HTTP 引擎」下支撑这么多业务域的差异化逻辑——这是 NestJS + Fastify 的真正考验。

概览页:16 家平台支持网格 + 5 大数据卡片,多平台对账全景

上面这张是这套系统的概览页——16 家平台支持网格 + 5 大数据卡片(启用店铺、账单总数、收入计划、运行中任务、费用计划)。所有这些能力都跑在 NestJS + Fastify 上。NestJS + Fastify 的真正考验,就是看你正在做的产品能不能在「统一的 HTTP 引擎」下支撑这么多业务域的差异化逻辑。

延伸阅读

如果你想深入了解 NestJS 在我们项目里的其他实践,可以参考以下资源:

  • NestJS 模块化设计的细节:参见项目内 .opencode/skills/income-reconciliation-development/SKILL.md(双子对账计划架构、模块边界设计)
  • 整体技术架构全景:参见项目内 .opencode/skills/ai-agent-development/SKILL.md(AI Agent 与业务模块的协作模式)

NestJS + Fastify 的选型是用工程纪律换长期收益的决策。希望这篇文章帮你建立完整的决策框架。

本文为原创内容,转载请注明出处:https://www.qeasy.cloud/insights/reconciliation/2-1-5-nestjs-fastify-vs-express

评论