NestJS 用了两年,我换了这个

11 阅读7分钟

如果你用 NestJS 超过一年,你一定经历过这样的下午:

新建一个最简单的用户管理模块,nest g resource users,然后在 users.module.tsusers.controller.tsusers.service.tscreate-user.dto.ts 四个文件之间反复跳转。写了一堆 @Injectable()@Controller()@Get()@ApiProperty()@ApiOperation(),回头一看 -- 业务逻辑就三行。

更痛的是 Swagger 文档。每个字段都要手动加 @ApiProperty({ description: '用户名', example: 'admin' }),改了 Entity 忘了改 DTO,线上文档和实际接口对不上,前端同事找你对了半小时。

还有 pnpm start:dev,改一行代码等 2-3 秒热重载。项目大了之后,DI 容器初始化 + 装饰器反射扫描,启动时间直接起飞。

我不是来黑 NestJS 的。NestJS 是 Node.js 生态里架构最完善的框架,没有之一。但"完善"和"适合"是两回事。用了两年之后,我做了个后台管理模板,把 NestJS 里那些真正需要的工程化能力用函数式的方式重新实现了一遍 -- 去掉装饰器、去掉 DI 容器、去掉样板代码,只留下核心。

技术栈:Hono + Vite + Drizzle ORM + PostgreSQL + Redis + Zod + OpenAPI + Casbin RBAC

为什么是 Hono

Hono 是一个超轻量的 Web 框架,核心只有 14KB,天然支持 Node.js / Bun / Cloudflare Workers 多运行时。它不是 Express 的重写,而是从零设计的现代框架 -- 原生支持 TypeScript 类型推导、OpenAPI 集成、中间件组合。

但 Hono 本身只是框架。NestJS 用户习惯的那些东西 -- 权限系统、ORM 集成、API 文档、项目结构规范、任务队列 -- Hono 原生都没有。

所以我做了这个模板:把 NestJS 级别的工程化能力,用函数式的方式搬过来。

代码对比:定义一个 CRUD 接口

这是全文最重要的部分。

路由定义

NestJS 的写法

@Controller('system/users')
@ApiTags('系统用户')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @ApiOperation({ summary: '获取用户列表' })
  @ApiQuery({ name: 'page', required: false, type: Number })
  @ApiQuery({ name: 'pageSize', required: false, type: Number })
  @ApiResponse({ status: 200, type: UserListResponseDto })
  findAll(@Query() query: ListQueryDto) {
    return this.usersService.findAll(query);
  }

  @Post()
  @ApiOperation({ summary: '创建用户' })
  @ApiBody({ type: CreateUserDto })
  @ApiResponse({ status: 201, type: UserResponseDto })
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}

Hono 模板的写法

// users.routes.ts
export const list = createRoute({
  tags: ["/system/users(系统用户)"],
  summary: "获取系统用户列表",
  method: "get",
  path: "/system/users",
  request: {
    query: RefineQueryParamsSchema,
  },
  responses: {
    [HttpStatusCodes.OK]: jsonContent(
      RefineResultSchema(systemUsersListResponseSchema), "列表响应成功"
    ),
  },
});

export const create = createRoute({
  tags: ["/system/users(系统用户)"],
  summary: "创建系统用户",
  method: "post",
  path: "/system/users",
  request: {
    body: jsonContentRequired(insertSystemUsersSchema, "创建系统用户参数"),
  },
  responses: {
    [HttpStatusCodes.CREATED]: jsonContent(
      RefineResultSchema(systemUsersResponseSchema), "创建成功"
    ),
  },
});

没有装饰器,纯对象声明。RefineQueryParamsSchemainsertSystemUsersSchema 都是 Zod Schema,同时定义了验证规则和 OpenAPI 文档。改一处,验证逻辑、TypeScript 类型、API 文档全部同步。

Handler 实现

// users.handlers.ts
export const create: SystemUsersRouteHandlerType<"create"> = async (c) => {
  const body = c.req.valid("json");    // 类型自动推导自 Zod Schema
  const { sub } = c.get("jwtPayload"); // JWT payload 类型安全

  const created = await createUser(body, sub);
  const userWithoutPassword = omit(created, ["password"]);

  return c.json(Resp.ok(userWithoutPassword), HttpStatusCodes.CREATED);
};

c.req.valid("json") 返回的类型是从 createRoute 中的 Zod Schema 自动推导的。Schema 改了字段,handler 里直接报红。这在 NestJS 里做不到 -- DTO 和 Controller 参数的类型一致性只靠开发者自觉。

路由注册

// users.index.ts -- 完整文件,就这些
import { createRouter } from "@/lib/core/create-app";
import * as handlers from "./users.handlers";
import * as routes from "./users.routes";

const systemUsersRouter = createRouter()
  .openapi(routes.get, handlers.get)
  .openapi(routes.update, handlers.update)
  .openapi(routes.remove, handlers.remove)
  .openapi(routes.create, handlers.create)
  .openapi(routes.list, handlers.list)
  .openapi(routes.saveRoles, handlers.saveRoles);

export default systemUsersRouter;

没有 Module 注册、没有 providers 数组、没有 imports 依赖图。export default 之后,框架通过 import.meta.glob 自动扫描挂载。新建一个目录、创建 xxx.index.ts,保存,Vite HMR 毫秒级生效 -- 不需要在任何地方"注册"这个模块。

Zod:一处定义,处处使用

NestJS 的 DTO 和 Entity 是两套东西,改了 Entity 忘了改 DTO 是最常见的 bug。这个模板里,Schema 只有一个源头:

从数据库表定义开始

// db/schema/admin/system/users.ts
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(),
});

// Drizzle 自动生成 Zod Schema
export const selectSystemUsersSchema = createSelectSchema(systemUsers, {
  username: schema => schema.meta({ description: "用户名" }),
});
export const insertSystemUsersSchema = createInsertSchema(systemUsers, {
  username: () => usernameField,
  password: () => passwordField,
}).omit({ id: true, createdAt: true, updatedAt: true });

路由层组合 Schema

// users.schema.ts
export const systemUsersResponseSchema = selectSystemUsersSchema.omit({
  password: true,
});

export const systemUsersPatchSchema = insertSystemUsersSchema.partial().refine(
  data => Object.keys(data).length > 0,
  { message: "至少需要提供一个字段进行更新" },
);

完整的数据流:

Drizzle 表定义
  → createSelectSchema / createInsertSchema 自动生成 Zod Schema
    → 路由层 pick/omit/extend 组合业务 Schema
      → createRoute() 自动生成 OpenAPI 3.1.0 规范
        → Scalar UI 在线文档 + 在线调试
      → Handler 类型自动推导(编译时检查)
      → Zod 4 中文校验错误(前端直接展示)

一个源头,六个消费者。 NestJS 需要 Entity + DTO + Swagger 装饰器三套东西维护同一份信息。

不需要 DI 容器

这是 NestJS 用户最难放下的心理负担 -- "没有 DI 容器怎么管理依赖?"

答案是:依赖图天然简单,不需要 DI 容器。

进程级资源 -- Singleton 模式(HMR 安全)

// 数据库连接、Redis 客户端等长连接
const redisClient = createSingleton("redis", () => new Redis(config), {
  destroy: client => client.quit(),
});

// 使用时直接 import,没有 @Inject()
import redisClient from "@/lib/services/redis";
import db from "@/db";

请求级数据 -- Hono Context

const { sub } = c.get("jwtPayload"); // 中间件写入,handler 读取,类型安全
const requestId = c.get("requestId");

复杂编排 -- Effect Layer

export const withLock = (key, effect) =>
  Effect.acquireUseRelease(
    Effect.tryPromise({ try: () => redlock.acquire([`lock:${key}`], 10000) }),
    () => effect,
    lock => Effect.promise(() => lock.release()),
  );

三层注入策略:进程级 Singleton + 请求级 Hono Context + 可组合 Effect Layer。覆盖后台管理系统 100% 的场景。@Injectable() + @Module({ providers: [] }) 只会增加间接层和注册仪式。

开发体验对比

维度NestJSHono 模板
新增 CRUD 模块5 文件(module/controller/service/dto/entity)4 文件(routes/handlers/index/types),无需注册
API 文档@ApiProperty 装饰器,容易遗漏Zod Schema 自动派生,改一处全同步
类型安全装饰器与实际类型可能不一致端到端 Zod 推导,编译时校验
依赖注入@Injectable + Module providersimport + Singleton
热重载秒级(反射 + DI 初始化)毫秒级(Vite HMR)
权限系统自己搭或第三方内置 Casbin RBAC + 审计日志
验证错误class-validator 英文消息Zod 4 原生中文错误消息
框架体积@nestjs/core ~563KB(全家桶含 common/platform-express 等更大)Hono ~14KB
类型检查tsctsgo(原生 Go 编译,极快)

开箱即用

不只是一个 demo,是一个生产就绪的模板:

  • 三层路由自动加载public(无认证)/ client(JWT)/ admin(JWT + RBAC + 审计日志),app.config.ts 一行配置
  • 声明式应用配置defineConfig() 驱动应用组装,入口文件只有 3 行:
    await bootstrap();
    export default await createApplication(config);
    
  • 通用查询引擎:内置 RefineQuery 支持分页/过滤/排序/联表,与 Refine 前端框架无缝对接
  • pg-boss 任务队列:PostgreSQL 原生的分布式安全后台任务
  • Redlock 分布式锁:基于 Effect-TS 的类型安全锁管理
  • S3 兼容存储:Cloudflare R2 / 阿里云 OSS / AWS S3
  • Cap.js 验证码:SHA-256 工作量证明,替代传统图片验证码
  • 自定义 Vite 插件:Zod 静态提升优化、资源监控、HMR 通知
  • tsgo 类型检查:TypeScript 原生 Go 编译器,类型检查速度提升 10 倍
  • 完善的测试:Vitest + Hono testClient,端到端类型安全
  • AI 驱动开发:CLAUDE.md + MCP 插件,Claude Code 深度理解项目架构

谁适合用这个

NestJS 更适合:超大型企业项目(几十个微服务),团队 Java/Spring 背景,需要 NestJS 生态的深度集成(GraphQL、微服务、WebSocket)。

这个模板更适合:中小型后台管理系统(1-5 人团队),追求开发效率和类型安全的全栈开发者,偏好函数式编程的团队,需要快速启动快速迭代的项目。

最后

这个模板不是要替代 NestJS,是给那些觉得"NestJS 太重了"的开发者一个新选择。

如果你看到这里还在犹豫,最好的方式就是 clone 下来跑一下。pnpm dev,打开 http://localhost:9999,Scalar 文档 UI 已经在等你了。

GitHub:github.com/zhe-qi/clho…

配套前端(Refine + Shadcn):github.com/zhe-qi/refi…

QQ 交流群:1076889416

觉得有用的话,给个 Star 就是最大的支持。