如果你用 NestJS 超过一年,你一定经历过这样的下午:
新建一个最简单的用户管理模块,nest g resource users,然后在 users.module.ts、users.controller.ts、users.service.ts、create-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), "创建成功"
),
},
});
没有装饰器,纯对象声明。RefineQueryParamsSchema 和 insertSystemUsersSchema 都是 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: [] }) 只会增加间接层和注册仪式。
开发体验对比
| 维度 | NestJS | Hono 模板 |
|---|---|---|
| 新增 CRUD 模块 | 5 文件(module/controller/service/dto/entity) | 4 文件(routes/handlers/index/types),无需注册 |
| API 文档 | @ApiProperty 装饰器,容易遗漏 | Zod Schema 自动派生,改一处全同步 |
| 类型安全 | 装饰器与实际类型可能不一致 | 端到端 Zod 推导,编译时校验 |
| 依赖注入 | @Injectable + Module providers | import + Singleton |
| 热重载 | 秒级(反射 + DI 初始化) | 毫秒级(Vite HMR) |
| 权限系统 | 自己搭或第三方 | 内置 Casbin RBAC + 审计日志 |
| 验证错误 | class-validator 英文消息 | Zod 4 原生中文错误消息 |
| 框架体积 | @nestjs/core ~563KB(全家桶含 common/platform-express 等更大) | Hono ~14KB |
| 类型检查 | tsc | tsgo(原生 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 就是最大的支持。