前言
2026 年的 Node.js 生态已经发生了翻天覆地的变化:
- Node.js 24 LTS / 25 Current 原生支持 TypeScript(
--experimental-strip-types已稳定) - Pure ESM 成为社区事实标准,CJS 逐渐退出历史舞台
- AI 编程从"辅助工具"变成"核心生产力",Claude Code、Cursor 重新定义了开发流程
- Zod 4 发布,schema-first 开发范式全面成熟
- Vite 8(底层 Rolldown)让后端开发也能享受前端级别的 HMR 体验
在这个背景下,我重新审视了 Node.js 后端框架的选型。最终,我选择了 Hono 而不是用了多年的 NestJS ,并基于它构建了一套生产级模板 clhoria-template。
这篇文章不是"Hono 入门教程",而是一个经历过 NestJS 项目的开发者,在 2026 年重新选型时的思考过程。
一、NestJS:曾经的最优解,现在的包袱
先说明立场:NestJS 是一个优秀的框架,它在 2019-2023 年间几乎是 Node.js 企业级开发的唯一选择。但 2026 年再看,它的一些核心设计决策已经成了负担。
1.1 Pure ESM 困境:被装饰器绑架的模块系统
NestJS 的核心依赖 reflect-metadata,这个库需要一行副作用导入:
typescript
import "reflect-metadata"; // 必须在入口文件最顶部
在 CJS 时代这不是问题。但在 Pure ESM 下,副作用导入的加载时序变得不可预测------ESM 的静态分析特性意味着导入顺序不再是你写的顺序,而是依赖图的拓扑序。
更深层的问题是 experimentalDecorators。这是 TypeScript 的 legacy 特性,TC39 的 Stage 3 装饰器提案在语义上完全不同(比如参数装饰器被砍掉了),而 NestJS 的 DI 系统重度依赖参数装饰器。2026 年了,NestJS 仍然要求你在 tsconfig.json 里开着这两个选项:
json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
这意味着你没法用 TypeScript 5.8 引入的 erasableSyntaxOnly(仅保留可擦除语法),也意味着你的代码本质上依赖一个已经偏离标准化方向的实验性特性。
社区包的 CJS/ESM 双包问题更是雪上加霜。typeorm、class-transformer 这些 NestJS 生态的核心库,ESM 支持至今仍是大量 GitHub Issue 的来源。Jest 在 ESM 下需要 --experimental-vm-modules,但这个 flag 在 Node.js 25 中仍然是实验性的,配合 NestJS 的装饰器元数据经常出现各种诡异问题。
而 Hono 呢? 原生 Pure ESM,零装饰器,零 reflect-metadata。你的 tsconfig.json 干干净净:
json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true
}
}
1.2 类型割裂:RTTI 是 JavaScript 的二等公民
NestJS 的验证层用 class-validator + class-transformer,这套方案有一个根本性的架构缺陷:你需要同时维护 TypeScript 类型和装饰器元数据两套系统,而这两者并不自动同步。
根因是 JavaScript 中运行时类型信息(RTTI)是二等公民。TypeScript 的类型在编译后被完全擦除,所以 NestJS 用装饰器"手动重建"了一套运行时类型系统。但这就意味着:
typescript
// NestJS: 改了类型忘了改装饰器?编译器不会报错,运行时才爆
export class CreateUserDto {
@IsString()
@MinLength(3)
@MaxLength(64)
username: string; // TS 类型
@IsString()
@MinLength(6)
password: string; // 如果把 string 改成 string | null,@IsString() 不会提醒你
@IsOptional()
@IsEnum(Gender)
gender?: Gender; // 加了 TS 类型的 optional,但忘了加 @IsOptional()?运行时必填
}
这类 bug 极其常见,而且很隐蔽------TypeScript 编译通过,单元测试可能也通过(因为测试数据恰好覆盖了 happy path),直到线上收到一个意料之外的请求才暴露。
社区已经意识到了这个问题------nestjs-zod 的出现本身就说明 class-validator 路线走到了瓶颈。但在 NestJS 框架内用 Zod,总有种"戴着镣铐跳舞"的感觉,很多地方还是要写 DTO class 来适配管道。
Hono + Zod 的方案从根源上解决了这个问题------Schema 即类型,单一数据源:
typescript
// Hono + Zod: Schema 定义 = 类型定义 = 验证规则 = OpenAPI 文档
// 改了 Schema,类型自动变,验证自动变,文档自动变
export const insertSystemUsersSchema = createInsertSchema(systemUsers, {
username: () => z.string().min(3).max(64),
password: () => z.string().min(6),
nickName: () => z.string().min(1).max(64),
}).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// 类型直接从 Schema 推导,永远同步
type CreateUserInput = z.infer<typeof insertSystemUsersSchema>;
一处修改,处处同步。TypeScript 编译器在编译时就能发现不一致。
1.3 过度工程化:简单 CRUD 不需要三层架构
NestJS 的 Angular 风格架构在面对简单 CRUD 时显得过于笨重。一个"获取用户列表" API,你需要:
typescript
// NestJS: 三层透传
// 1. Controller(接收请求)
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(@Query() query: ListQueryDto) {
return this.usersService.findAll(query); // 透传给 Service
}
}
// 2. Service("业务逻辑",其实大部分时候只是透传)
@Injectable()
export class UsersService {
constructor(private readonly usersRepo: UsersRepository) {}
findAll(query: ListQueryDto) {
return this.usersRepo.findAll(query); // 透传给 Repository
}
}
// 3. Repository(数据访问)
@Injectable()
export class UsersRepository {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
findAll(query: ListQueryDto) {
return this.repo.find({ /* ... */ }); // 终于到数据库了
}
}
// 4. 还有 Module 注册
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
imports: [TypeOrmModule.forFeature([User])],
})
export class UsersModule {}
四个文件,大量样板代码,Service 层在简单场景下就是纯透传。有人说"以后业务复杂了就需要了"------问题是 80% 的后台管理 CRUD 永远不会变复杂。
Hono 的垂直切片架构让简单的事情保持简单:
typescript
// Hono: 路由定义即 OpenAPI 文档
export const list = createRoute({
tags: ["系统用户"],
summary: "获取系统用户列表",
method: "get",
path: "/system/users",
request: { query: RefineQueryParamsSchema },
responses: {
[HttpStatusCodes.OK]: jsonContent(
RefineResultSchema(systemUsersListResponseSchema),
"列表响应成功"
),
},
});
// Handler 直接操作数据库,不需要 Service 透传
export const list: RouteHandler<typeof routes.list> = async (c) => {
const query = c.req.valid("query");
const [error, result] = await executeRefineQuery({
table: systemUsers,
queryParams: query,
});
if (error) return c.json(Resp.fail(error.message), HttpStatusCodes.INTERNAL_SERVER_ERROR);
return c.json(Resp.ok(result.data), HttpStatusCodes.OK);
};
80% 的简单 CRUD 用 Transaction Script(Handler 直接操作 DB),20% 的复杂业务再引入 DDD 或 Effect-TS。 这才是合理的复杂度分配。
二、Hono:回归 Web 标准
2.1 Web Standard API
Hono 基于 Web Standard API(Request/Response/fetch)构建,这意味着同一套代码可以跑在:
- Node.js(通过
@hono/node-server) - Bun
- Deno
- Cloudflare Workers
- AWS Lambda
- Vercel Edge Functions
这不是一个理论上的优势------我们的项目在开发环境用 Node.js,部分轻量 API 直接部署到 Cloudflare Workers,零代码修改。
2.2 Zod 全链路类型安全
在 clhoria-template 中,类型安全贯穿整个数据流:
markdown
Drizzle 表定义 → drizzle-zod 生成 Schema → 路由层组合 Schema → OpenAPI 3.1 文档 → Scalar UI
↓ ↓ ↓ ↓
PostgreSQL z.infer<> 推导类型 请求/响应验证 前端类型生成
一处定义,四处同步。 改了数据库字段,TypeScript 编译器会在所有使用该字段的地方报错,直到你同步修改完。
typescript
// 1. Drizzle 表定义(数据库层)
export const systemUsers = pgTable("system_users", {
...baseColumns,
username: varchar({ length: 64 }).notNull().unique(),
password: text().notNull(),
nickName: varchar({ length: 64 }).notNull(),
status: statusEnum().default(Status.ENABLED).notNull(),
});
// 2. drizzle-zod 自动生成基础 Schema(验证层)
export const selectSystemUsersSchema = createSelectSchema(systemUsers, {
id: schema => schema.meta({ description: "用户ID" }),
username: schema => schema.meta({ description: "用户名" }),
});
// 3. 路由层组合(API 层)
export const systemUsersResponseSchema = selectSystemUsersSchema.omit({ password: true });
// 4. OpenAPI 路由定义(文档层)
export const get = createRoute({
summary: "获取系统用户详情",
method: "get",
path: "/system/users/{id}",
request: { params: IdUUIDParamsSchema },
responses: {
[HttpStatusCodes.OK]: jsonContent(systemUsersResponseSchema, "成功"),
},
});
2.3 原生 Pure ESM + erasableSyntaxOnly
项目的 tsconfig.json 启用了 erasableSyntaxOnly,这意味着 TypeScript 代码只使用可擦除语法------类型注解、接口、类型别名等编译后会被完全擦除的特性。没有装饰器、没有 enum(用 const 对象替代)、没有 namespace。
好处是什么?Node.js 24+(包括 LTS)可以直接运行你的 .ts 文件 ,不需要编译步骤。开发环境用 Vite(获得 HMR),生产环境用 Vite build(获得 Tree Shaking 和优化),但紧急情况下 node --experimental-strip-types src/index.ts 也能直接跑。
注:本模板开发环境使用 Node.js 25 Current,生产部署建议使用 Node.js 24 LTS。
三、AI 友好架构:为什么这很重要
这是我选 Hono 的一个重要但很少有人讨论的原因:在 AI 编程时代,框架的"AI 友好度"直接影响开发效率。
3.1 AI 的核心瓶颈:没有持久记忆
Claude Code、Cursor 这类 AI 编程工具有一个根本限制:每次对话都是从零开始,AI 没有持久记忆。 它不知道你的项目用了什么框架、遵循什么规范、哪些模式该用哪些该避免。
clhoria-template 的解法是 CLAUDE.md------一个项目级的"上下文锚点"文件:
markdown
# CLAUDE.md
## Stack
Hono + Node.js 25 + PostgreSQL(Drizzle snake_case) + Redis(ioredis) + JWT(admin/client) + Casbin RBAC + Zod(Chinese errors) + OpenAPI 3.1.0(Scalar) + Vitest + vite
## Architecture
**Route Tiers**: `/api/public/*` (no auth) | `/api/client/*` (JWT) | `/api/admin/*` (JWT+RBAC+audit)
**Auto-load**: `import.meta.glob` from `routes/{tier}/**/*.index.ts`
## Critical Rules
return c.json(Resp.ok(data), HttpStatusCodes.OK);
return c.json(Resp.fail("error"), HttpStatusCodes.BAD_REQUEST);
logger.info({ userId }, "[Module]: message"); // data object FIRST
每次对话开始,AI 会自动读取这个文件,快速建立对项目的"工作记忆"。注意------我说的不是 AI "理解"了你的架构,本质上它是在做模式匹配:你给它足够精确的约束条件,它就能在约束下稳定生成符合架构意图的代码。
3.2 SDD:用结构化文档约束 AI 的生成边界
我们采用 Spec-Driven Development(规范驱动开发) 方法论:
Spec → 生成代码 → 生成测试 → 循环优化 → 模块文档
先写规范文档(docs/{feature}/spec.md),明确需求、架构、测试策略,然后让 AI 基于规范生成代码。这样做的好处是:
- 规范即约束------AI 不会"发挥创意"生成你不想要的代码
- 规范即验收标准------生成的代码必须满足规范中定义的所有条件
- 规范即文档------代码写完了,文档也写完了
3.3 为什么 Hono 比 NestJS 更 AI 友好
这是一个微妙但重要的点:显式依赖 + 文件约定比隐式 DI 更利于 AI 做模式匹配。
在 NestJS 中,依赖关系是隐式的:
typescript
// NestJS: AI 需要理解 DI 容器的注入机制才能正确生成代码
@Injectable()
export class OrderService {
constructor(
private readonly userService: UsersService, // 从哪来的?
private readonly paymentService: PaymentService, // Module 注册了吗?
@InjectRepository(Order) private repo: Repository<Order>, // TypeORM 注入
) {}
}
AI 要正确生成这段代码,需要同时"知道":Module 的 imports/providers 配置、其他 Service 的注入 Token、TypeORM 的实体注册。这些信息分散在多个文件中,AI 经常搞错。
在 Hono 中,依赖关系是显式的:
typescript
import { eq } from "drizzle-orm";
// Hono: 直接导入,AI 一目了然
import db from "@/db";
import { systemUsers } from "@/db/schema";
export const get: RouteHandler = async (c) => {
const { id } = c.req.valid("param");
const user = await db.query.systemUsers.findFirst({
where: eq(systemUsers.id, id),
});
return c.json(Resp.ok(user), HttpStatusCodes.OK);
};
没有隐式注入,没有装饰器魔法,每一个依赖都是一个 import 语句。AI 只需要看文件顶部就知道所有依赖,模式匹配的准确率大幅提升。
3.4 文件约定 > DI 容器
clhoria-template 的 CRUD 模块遵循严格的文件约定:
bash
routes/{tier}/{feature}/
├── {feature}.index.ts # 路由注册入口
├── {feature}.routes.ts # OpenAPI 路由定义
├── {feature}.handlers.ts # 业务处理器
├── {feature}.types.ts # 类型定义
├── {feature}.schema.ts # 路由级 Zod Schema(可选)
├── {feature}.helpers.ts # 辅助函数(可选)
└── __tests__/ # 测试目录
这个约定对 AI 来说就是一个"模板"------你说"帮我创建一个订单管理模块",AI 直接按照这个结构生成 6 个文件,每个文件的职责明确,互相之间的引用关系固定。它不需要"理解"你的架构,只需要"复制"这个模式并填入业务逻辑。
相比 NestJS 的 @Module() + @Controller() + @Injectable() + providers + imports 这套注册仪式,文件约定的模式匹配难度低了一个数量级。
四、实战架构展示
下面用 clhoria-template 的真实代码展示完整架构。
4.1 声明式应用配置
typescript
// app.config.ts
import { defineConfig } from "@/lib/core/define-config";
export default defineConfig({
prefix: "/api",
openapi: {
enabled: env => env.NODE_ENV !== "production",
docEndpoint: "/doc",
scalar: {
theme: "kepler",
layout: "modern",
defaultHttpClient: { targetKey: "js", clientKey: "fetch" },
},
},
tiers: [
{ name: "public", title: "公共API文档" },
{ name: "client", title: "客户端API文档", token: "your-client-token" },
{ name: "admin", title: "管理端API文档", token: "your-admin-token" },
],
});
三层路由分级:public(无认证)、client(JWT)、admin(JWT + RBAC + 审计日志),通过一个配置文件声明,框架自动组装。
4.2 路由自动加载
typescript
// 框架内部,开发者无需关心
const allRoutes = import.meta.glob<{ default: AppOpenAPI }>(
"../../routes/**/*.index.ts",
{ eager: true },
);
新增模块只需在 routes/{tier}/{feature}/ 目录下创建文件,保存后 Vite HMR 毫秒级生效,不需要任何手动注册。
4.3 中间件声明
每个 Tier 有独立的中间件文件,支持条件跳过:
typescript
// routes/admin/_middleware.ts
export default defineMiddleware([
{
handler: jwt({ secret: env.ADMIN_JWT_SECRET, alg: "HS256" }),
except: c => ["/auth/login", "/auth/refresh"].some(p => c.req.path.endsWith(p)),
},
{
handler: authorize, // Casbin RBAC
except: c => c.req.path.includes("/auth"),
},
{
handler: operationLog(), // 操作审计日志
except: c => c.req.path.includes("/auth"),
},
]);
defineMiddleware + except 模式,比 NestJS 的 @UseGuards() + @Public() 装饰器组合更直观。
4.4 全链路类型安全的 CRUD
以系统用户模块为例,展示从数据库到 API 文档的完整链路:
数据库表定义:
typescript
// db/schema/admin/system/users.ts
export const systemUsers = pgTable("system_users", {
...baseColumns, // id(UUIDv7), createdAt, updatedAt, createdBy, updatedBy
username: varchar({ length: 64 }).notNull().unique(),
password: text().notNull(),
nickName: varchar({ length: 64 }).notNull(),
status: statusEnum().default(Status.ENABLED).notNull(),
});
路由定义(同时也是 OpenAPI 文档):
typescript
// routes/admin/system/users/users.routes.ts
export const create = createRoute({
tags: ["系统用户"],
summary: "创建系统用户",
method: "post",
path: "/system/users",
request: {
body: jsonContentRequired(insertSystemUsersSchema, "创建参数"),
},
responses: {
[HttpStatusCodes.CREATED]: jsonContent(systemUsersResponseSchema, "创建成功"),
[HttpStatusCodes.UNPROCESSABLE_ENTITY]: jsonContent(respErrSchema, "参数错误"),
},
});
处理器:
typescript
// routes/admin/system/users/users.handlers.ts
export const create: SystemUsersRouteHandlerType<"create"> = async (c) => {
const body = c.req.valid("json"); // 自动验证 + 类型推导
const { sub } = c.get("jwtPayload"); // 类型安全的上下文
const created = await createUser(body, sub);
const userWithoutPassword = omit(created, ["password"]);
return c.json(Resp.ok(userWithoutPassword), HttpStatusCodes.CREATED);
};
路由注册:
typescript
// routes/admin/system/users/users.index.ts
const router = createRouter()
.openapi(routes.list, handlers.list)
.openapi(routes.create, handlers.create)
.openapi(routes.get, handlers.get)
.openapi(routes.update, handlers.update)
.openapi(routes.remove, handlers.remove);
export default router;
export default 路由实例,框架通过 import.meta.glob 自动加载。从创建文件到 API 可访问 + 文档可查看,全程零配置。
4.5 开发体验
Vite 8 HMR:代码保存后毫秒级生效。后端开发终于能享受前端的开发体验了。
Scalar 文档 :访问 http://localhost:9999 即可看到带有在线调试功能的 API 文档,支持多 Tier 切换,自动填充认证 Token。
环境变量类型安全:
typescript
// src/env.ts
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().default(9999),
DATABASE_URL: z.string(),
ADMIN_JWT_SECRET: z.string().min(32, "JWT密钥长度至少32字符"),
// ...
});
export default parseEnvOrExit(EnvSchema); // 启动时验证,不合法直接退出
统一响应格式:
typescript
// 成功
return c.json(Resp.ok(data), HttpStatusCodes.OK);
// 失败
return c.json(Resp.fail("用户不存在"), HttpStatusCodes.NOT_FOUND);
// Zod 错误自动格式化
return c.json(Resp.fail(zodError), HttpStatusCodes.UNPROCESSABLE_ENTITY);
五、全方位对比
| 维度 | NestJS | Hono (clhoria-template) |
|---|---|---|
| 模块系统 | CJS 为主,ESM 可用但摩擦较大 | 原生 Pure ESM |
| 类型安全 | class-validator 装饰器,类型与验证割裂 | Zod schema 即类型,单一数据源 |
| OpenAPI | @nestjs/swagger 额外装饰器 |
@hono/zod-openapi 代码即文档 |
| 路由注册 | @Module() + @Controller() 装饰器 |
import.meta.glob 自动扫描 |
| 中间件 | 装饰器守卫 + 拦截器 + 管道 | 函数式中间件 + except 条件跳过 |
| 依赖注入 | IoC 容器 + 装饰器注入 | 模块单例 + Hono Context + 可选 Effect Layer |
| 开发热重载 | Webpack/SWC,需要重启 | Vite HMR,毫秒级 |
| 运行时 | 仅 Node.js(Express/Fastify) | Node/Bun/Deno/CF Workers/Lambda |
| 启动速度 | 较慢(DI 容器初始化 + 装饰器元数据解析) | 极快(无反射开销) |
| AI 友好度 | 隐式 DI 依赖,AI 容易搞错注入关系 | 显式 import,文件约定,AI 一目了然 |
| 学习曲线 | 中高(需理解 Angular 风格 DI + 装饰器) | 低(纯 TypeScript + 函数式) |
| TypeScript 5.8+ 兼容 | 需要 experimentalDecorators | 完全兼容 erasableSyntaxOnly |
| 包体积 | 较大(框架 + reflect-metadata + class-*) | 极小(Hono 核心 ~14KB) |
六、什么时候该用 NestJS
公平起见,NestJS 在以下场景仍然是合理选择:
- 大型团队需要强约束:NestJS 的"规定动作多"反而是优势,50 人团队不可能人人写出优雅的函数式代码,装饰器 + DI 容器 + 严格分层能兜底代码质量。
- 重度依赖 NestJS 生态 :如果你深度使用
@nestjs/microservices、@nestjs/graphql、@nestjs/bull这些官方模块,迁移成本很高。 - 团队有 Angular/Java Spring 背景:NestJS 的 DI + 装饰器模式对这类团队来说是零学习成本。
- 已有大型 NestJS 项目:在跑的项目别轻易重写,维护好比什么都强。
七、总结
2026 年选 Node.js 后端框架,我的判断标准是:
- Pure ESM 原生支持 ------ 不想再和 CJS/ESM 双包问题纠缠
- 类型安全单一数据源 ------ Schema 即类型即验证即文档,一处修改处处同步
- AI 友好架构 ------ 显式依赖 + 文件约定,让 AI 在约束下稳定生成代码
- 合理的复杂度 ------ 80% CRUD 保持简单,20% 复杂业务按需引入高级模式
Hono 在这四个维度上都胜出。它不是 NestJS 的"替代品"------它代表的是一种不同的架构哲学:用 Web 标准而不是框架抽象,用类型推导而不是运行时反射,用文件约定而不是 DI 容器,用 AI 协作而不是纯人工编码。
如果你也在考虑 2026 年的技术选型,不妨试试 Hono。
模板地址:clhoria-template
配套前端(Refine + Shadcn):refine-project