Agent Developer Roadmap
全栈后端

Node.js 后端对照结果:Fastify 契约与 NestJS 组织

核验日期:2026-07-16。本文以 Node.js 24 LTS、Fastify 5 与 NestJS 11 作为主要讲解版本。Fastify 5 要求 Node 20+ 并要求完整 JSON Schema;NestJS 11 同样要求 Node 20+。生产采用受支持 Node LTS,并核对框架 Release/LTS。

Node 后端对前端开发者的优势是语言、包和调试经验可迁移;风险也是过度熟悉:把任意对象当输入、把 event loop 当无限并发、把 TypeScript 当运行时验证、把 controller 直接连 ORM。本文用 Fastify 展开请求热路径,用 NestJS 解释大型应用的装配边界;两者不是必须叠加的“标准架构”。

请求链与框架所有权

Node 拥有 socket、event loop、stream 和进程生命周期;Fastify 拥有路由、hook、schema 编译、request/reply 和 plugin encapsulation;业务代码拥有不变量;驱动拥有连接协议;部署拥有资源和信号。NestJS 可在 Express 或 Fastify adapter 上提供 module、DI、guard、pipe、interceptor、filter,但抽象不能消除底层 adapter 差异。

Fastify 5:schema 是协议而非装饰

import Fastify from "fastify";

const app = Fastify({ logger: true, requestIdHeader: "x-request-id" });

const querySchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    query: { type: "string", maxLength: 100 },
    limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
  },
} as const;

app.get("/technologies", {
  schema: {
    querystring: querySchema,
    response: {
      200: {
        type: "object",
        required: ["items", "dataVersion"],
        properties: {
          items: { type: "array", items: { type: "object" } },
          dataVersion: { type: "string" },
        },
      },
    },
  },
}, async (request) => {
  const { query = "", limit = 20 } = request.query as { query?: string; limit?: number };
  return catalog.list({ query, limit });
});

await app.listen({ host: "127.0.0.1", port: 3000 });

环境为 Node 24、Fastify 5;安装使用 pnpm,运行后请求 /technologies?limit=200 应得到验证失败,合法请求得到固定 response shape。示例的 item schema 为节省篇幅省略字段,生产契约必须完整列出,防止未审查字段泄露。

Fastify 使用 Ajv 类 JSON Schema validator 和高性能 serializer。TypeScript assertion 只帮助编辑器,恶意 JSON 仍由 schema 拒绝。输入与输出 schema 分开:输入允许的字段不等于输出,数据库实体也不等于公开 DTO。additionalProperties 策略、coercion、默认值、日期和 bigint 必须明确;自动 coercion 可能把字符串变数字,方便但会改变协议。

Schema 编译与动态输入

schema 通常在启动时编译,不要把用户输入变成 schema,也不要在请求热路径生成大量独特 schema。正则和 format 可能造成 CPU 风险,审查复杂表达式。验证失败由统一 error handler 映射为项目 problem 格式,不把 Ajv 内部路径直接稳定成公共 API。

响应 serialization schema 同时是性能和安全边界:只输出声明字段,降低意外 secret 泄露。但 schema 缺字段会静默丢失预期数据,因此 contract test 必须比较完整 body。不能只以“返回 200”验收。

Plugin encapsulation 与依赖生命周期

Fastify plugin 创建封装作用域,decorator/hook/routes 可以只对后代可见。数据库连接、repository、auth 和 routes 按真实生命周期装配;不要为每个文件建 plugin。

import fp from "fastify-plugin";

export default fp(async function databasePlugin(app) {
  const pool = createPool({ connectionString: app.config.databaseUrl });
  app.decorate("db", pool);
  app.addHook("onClose", async () => pool.end());
});

共享能力若要穿过 encapsulation,使用明确 plugin metadata/fastify-plugin。启动连接失败应让 readiness/启动失败,不要继续运行到首个请求。关闭 hook 在停止接收请求后释放 pool;migration 不应由每个副本无协调同时执行。

NestJS:何时需要容器与模块

NestJS 适合需要统一 module/DI、guard、pipe、interceptor、exception filter、OpenAPI、测试装配和团队规范的应用。小只读服务使用 Fastify 函数与 plugin 已足够;引入 Nest 会增加反射、decorator、启动和学习成本。

@Controller("technologies")
export class TechnologyController {
  constructor(private readonly catalog: CatalogService) {}

  @Get()
  list(@Query() query: ListTechnologyQuery) {
    return this.catalog.list(query);
  }
}

Controller 只适配 HTTP,service 不依赖 Request/Response。Pipe 完成运行时验证/转换,Guard 做认证授权,Interceptor 做跨切面包装,Filter 映射异常。不要把业务授权放在 interceptor 的字符串 metadata 拼装中;核心 service 仍接收 principal/resource 并验证 tenant。

Nest module 是依赖图边界,不等于业务领域边界。动态 module 在 v11 的 identity/dedup 行为变化会影响测试中的 provider 实例,升级时根据官方 migration guide 验证。默认 singleton provider 不能保存 request-specific mutable state;request scope 会增加对象创建和传播成本,只为确有需要的上下文使用。

TypeScript 的四层正确性

  1. TypeScript 编译期类型:阻止本项目中明显错误,不存在于 runtime。
  2. Runtime schema:验证 HTTP/queue/config 的不可信 bytes。
  3. 数据库 constraint:unique、foreign key、check、not null 对并发写兜底。
  4. 业务不变量:跨实体/权限/状态迁移,由 use case + transaction 保证。

不要从同一 class decorator 自动生成所有层然后假设语义一致。optional 在 patch、create、database nullable、response 中含义不同。日期是带时区字符串还是 epoch,64 位整数是否超过 JS 安全范围,都要在协议显式。

异步 I/O、event loop 与连接池

Node handler 的 async 不会把 JSON.parse、加密、模板或同步 ORM 移出主线程。event-loop p99 高时用 CPU profile/GC/ELU;外部等待高时看连接池 wait、上游 trace。Promise.all 无界 fan-out 会耗尽连接、内存和下游。

const results = [];
for (const batch of chunks(ids, 20)) {
  results.push(...await Promise.all(batch.map((id) => repository.get(id, signal))));
}

固定批次只是简单上限;真正实现应有 deadline、错误策略和可取消队列。数据库 pool 大小乘副本数不能超过数据库预算。请求取消要传播到 fetch/driver;并非所有数据库驱动都支持立即取消,超时后连接是否可复用需测试。

CPU 任务使用有界 worker pool 或外部任务服务,不按请求创建 worker。AsyncLocalStorage 适合 request/trace context,不保存业务状态。跨 worker/queue 显式传 trace/idempotency。

数据库、事务与 ORM

Node 可选原生 driver、query builder 或 ORM。选型看 SQL 表达、类型映射、migration、transaction、pool、observability 和团队,不按“类型安全”单项决定。ORM 类型无法证明数据库中没有并发冲突。

await db.transaction(async (tx) => {
  const inserted = await tx.idempotency.tryInsert({ key, requestHash });
  if (!inserted) return tx.idempotency.getResult(key);
  const item = await tx.technologies.create(command);
  await tx.outbox.insert({ type: "TechnologyCreated", aggregateId: item.id });
  await tx.idempotency.complete(key, item);
  return item;
});

代码表达逻辑顺序,实际 API 取决于库。unique constraint 防相同 key 并发;requestHash 防 key 被不同 payload 重用;业务写、outbox、结果同事务。不要在事务内调用 HTTP broker;publisher 在提交后异步发送并标记。

transaction callback 被驱动自动 retry 时,回调必须无外部副作用。隔离级别按异常选择;读取后写入的检查需要 constraint、锁或 serializable/retry,不能只靠 JavaScript if。

缓存

进程内 Map 仅单实例,必须有容量/TTL;Redis 等共享 cache 也不是权威数据。cache-aside:读 cache,miss 查 DB,写带版本 key;写路径提交 DB 后失效/更新。删除失败会短暂陈旧,定义窗口并记录指标。不要把用户/tenant 数据放入缺范围 key。

缓存 JSON parse 仍占 event loop CPU。大对象/高并发要测序列化与内存。single-flight 只合并同实例或使用分布式锁,锁有 lease、owner 和超时;源站也要限流,避免锁失效后击穿。

认证与授权

认证 middleware/guard 验证 token 签名、issuer、audience、expiry、算法/key rotation,生成最小 principal。授权依据 action、resource、tenant 和当前状态,最好在 service/repository 查询约束中体现,防先查出跨租户数据再过滤。401/403/404 策略统一。

cookie session 处理 Secure、HttpOnly、SameSite、CSRF 与 rotation;Bearer token 防日志和 URL 泄露。不要自己实现密码哈希/JWT crypto。permission cache 失效和权限变更窗口要明确。

错误、日志与可观测性

定义 AppError(code, status, details, cause) 或判别联合;只有预期错误映射 4xx/503,未知 bug 记录 cause 并返回通用 500。不能按错误 message 字符串判断。Node Error cause 保留链;日志序列化 whitelist 字段。

Fastify logger 具 request id,上下文记录 route pattern(非原始高基数 URL)、status、duration、bytes、user/tenant 的不可逆或内部 id。metrics label 不放 request id/user/query。OpenTelemetry/agent 必须验证 AsyncLocalStorage、worker 和数据库 instrumentation;采样不丢错误 trace。

测试

Fastify inject 可不监听端口测试完整路由/hook/schema;Nest TestingModule 测装配,但不要把所有测试变成沉重容器。核心 service 使用普通对象和 fake clock/id generator。数据库集成用真实数据库;contract suite 黑盒运行五个实现。

const response = await app.inject({ method: "GET", url: "/technologies?limit=101" });
assert.equal(response.statusCode, 400);
assert.equal(response.json().code, "INVALID_QUERY");

还要测重复 idempotency、两个并发写、pool exhausted、upstream timeout、client abort、SIGTERM in-flight 和旧/新 schema rollout。mock ORM 返回值不能证明 SQL/constraint。

配置、部署与优雅关闭

启动时验证 config,区分 public config/secret;缺失即失败,不在请求中懒读。容器以非 root、只读 rootfs、固定 Node/lock,镜像不含 dev secret/source map(除受控上传)。health 与 readiness 分开。

SIGTERM:标记 unready,停止新请求,关闭 keep-alive/idle,等待有界 in-flight,停止 consumer/worker,关闭 pool,flush 有界 telemetry,超时非零退出。Nest/Fastify lifecycle hooks 只是入口,仍需与代理 grace period 演练。

migration 使用 expand/contract:先加向后兼容 schema,发布能读新旧的代码,回填,再收缩。不能启动每个 pod 自动执行 destructive migration。回滚要考虑数据已由新版本写入。

Fastify 与 NestJS 选择

约束Fastify 直接使用NestJS + adapter
小服务/少团队更少抽象、启动快、边界可见固定成本可能不值
多模块/统一组织plugin 足够但规范需团队约定module/DI/guard/filter 更一致
性能热路径较直接,schema/serializer 强adapter/DI 有成本,需实测
团队来自 Angular/Java需学习 plugin/hookdecorator/DI 心智更熟悉
高度自定义显式 hook/plugin可扩展但层次更多

选择 Nest 不是选择不同 Node 并发模型。底层 Fastify adapter 的 schema、request/reply 与 Express adapter 不完全相同;跨 adapter 切换需 contract/performance test。

故障排查

高频错误:TypeScript DTO 没运行时验证;response schema 丢字段;Nest request scope 滥用;Promise.all 压垮 pool;transaction 中发 HTTP;catch 后继续未知状态;重试非幂等写;SIGTERM 立即 exit;日志打印 token/body;migration 与旧版本不兼容。

版本与来源

交付检查

保存 Node/Fastify/Nest 精确版本、完整 schemas/OpenAPI、contract tests、plugin/module graph、pool/transaction/idempotency/outbox 证据、auth negative tests、event-loop/pool/DB trace、SIGTERM 时间线、migration expand/contract、p95/p99/CPU/RSS 与回滚。若只是 controller 和 ORM CRUD,没有这些边界,就还不是生产后端对照结果。

运行演练:过载、取消与部分响应

先建立三条只在测试环境启用的故障路径:同步计算占用主线程、数据库连接延迟、上游返回慢流。对每条固定最大持续时间和调用权限。对照结果负载记录 event-loop delay、ELU、CPU、RSS、pool wait、in-flight 与 p99;一次只注入一种故障。

同步计算时所有请求延迟一起升高,profile 应指向函数;若只看 CPU 平均可能被多核稀释。数据库慢时 event loop 未必高,但 pool wait/in-flight 上升;增加 Node 副本会乘连接并恶化。慢流时检查 backpressure:响应内存不应随完整 body×并发增长;client abort 后上游和转换是否取消。已写 header 后发生错误不能改成 JSON problem,只能结束 stream并记录稳定事件。

设置应用并发上限/队列。队列满返回429或503并按语义选择Retry-After,不继续接受到OOM。限流维度考虑IP、principal、tenant与route,但代理来源可信链要配置,不能相信任意X-Forwarded-For。全局、实例和下游bulkhead结合,避免单tenant占满pool。

API兼容与版本化

优先向后兼容演进:添加可选字段,客户端忽略未知;删除/重命名/类型改变走新版本或迁移窗口。不要把URL版本、header版本和npm package版本混用。OpenAPI diff只是提示,contract consumer测试确认真实使用。

部署新旧版本同时存在时,数据库schema和消息必须双向兼容。新增not-null字段先nullable/default+回填,再让代码写入,最后constraint;事件添加字段时consumer先兼容。response serializer schema也要允许旧实例读取新数据。回滚前检查新版本是否已写入旧代码无法解析的值。

ETag/If-None-Match用于缓存读取,If-Match/版本字段可做optimistic concurrency。409/412选择按HTTP语义统一。不要只在Node内存维护版本。分页cursor包含稳定排序键和data/schema version,验证签名/长度;更换排序时旧cursor明确过期。

进程、集群与部署拓扑

Node单进程利用一个主JavaScript线程,CPU配额大于一核不自动让handler并行。可由容器多副本或worker/cluster利用多核。优先平台副本,因为故障隔离、滚动和指标清楚;单容器多worker会乘pool并使日志/健康复杂。CPU任务才用worker pool,普通I/O请求不需worker。

每实例资源预算包含V8 heap、Buffer/native、code、worker、连接和sidecar;--max-old-space-size不是容器总内存。接近限制时heap snapshot可能造成额外OOM,预先设计采样/profile和诊断报告。uncaught exception后进程状态未知,记录并退出,让supervisor拉起;不能无限“保持可用”。

部署证据包含artifact/lock/Node/framework/schema版本、config hash(不含secret)、migration、feature flags。feature flag也需要owner/expiry,不长期保留双实现。canary比较error code/p99/ELU/RSS/pool,不只健康。

NestJS大型项目边界

按业务能力组织module,而非Controllers/Services/Repositories全局技术目录。module导出最小public providers,内部实现不被其他模块随意注入。跨module交互用明确service/interface或event,不循环forwardRef成为常态。循环依赖通常说明owner不清。

CQRS/event sourcing不是Nest应用必需。普通transactional service足够时不加command bus十层。只有读写模型、异步流程和审计确有需求才引入,并定义一致性/失败。decorator metadata方便但隐藏控制流,关键权限/事务在测试和文档显式。

供应链与运行时更新

pnpm lock frozen安装,package scripts审计,production依赖最小,插件拥有进程权限。Fastify/Nest plugin/module升级既看semver也看安全Release;Fastify LTS说明安全修复有时可能在minor带breaking变化,不能冻结易受攻击版本回避。Node LTS patch同样纳入镜像更新。

升级测试包括schema compiler/serializer、HTTP parser、auth/crypto、ORM/driver/native addon、OpenTelemetry、source map和shutdown。双版本CI与staging,保留旧镜像/lock。依赖漏洞若不可利用也要记录证据和复审日期,不永久忽略。

返回专栏导读 · 下一篇:Python / FastAPI

在 GitHub 上编辑此页

本页目录