Hono 工程化实战 07:项目模板、Monorepo 和 CI 流程
wxk1991 Lv5

Hono 工程化实战 07:项目模板、Monorepo 和 CI 流程

一个 Hono 项目能不能长期维护,和第一天的目录结构关系很大。

小 demo 可以一个 index.ts 写完。

但真实项目很快会长出:

1
2
3
4
5
6
7
8
9
10
路由
middleware
service
repository
schema
测试
脚本
配置
前端客户端
部署文件

如果一开始没有边界,后面就会变成:

1
2
3
4
5
所有逻辑塞 routes
所有类型到处复制
前后端各写一份接口定义
测试不知道怎么 mock
CI 只会 npm install

这篇文章讲 Hono 项目如何搭一个可维护的工程模板,包括目录结构、monorepo、共享类型、测试和 CI。

技术栈快照时间:2026-06-23。Hono RPC、Testing Helper、各运行时部署方式会更新,生产使用前以官方文档为准。


一、先从单应用结构开始

如果只有一个 API,可以先这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
src/
app.ts
index.ts
routes/
posts.ts
auth.ts
middleware/
auth.ts
request-id.ts
schemas/
post.ts
auth.ts
services/
post-service.ts
auth-service.ts
repositories/
post-repository.ts
user-repository.ts
errors/
app-error.ts
config/
runtime-config.ts
tests/
routes/
services/

app.ts 负责组装 Hono:

1
2
3
4
5
6
7
8
9
10
11
12
import { Hono } from 'hono'
import { authRoutes } from './routes/auth'
import { postRoutes } from './routes/posts'

export function createApp(deps: AppDeps) {
const app = new Hono()

app.route('/api/auth', authRoutes(deps))
app.route('/api/posts', postRoutes(deps))

return app
}

index.ts 负责运行时入口。

这样测试可以直接 import createApp(),部署入口也清楚。


二、routes 不要写成万能层

很多项目最大的问题是 route 太胖。

错误方向:

1
2
3
4
5
6
7
8
app.post('/api/posts', async (c) => {
// 校验
// 鉴权
// SQL
// 业务规则
// 调第三方
// 拼响应
})

route 层应该主要做:

1
2
3
4
读取请求
调用 schema 校验
调用 service
转换响应

示例:

1
2
3
4
5
6
7
8
9
10
11
12
export function postRoutes(deps: AppDeps) {
const app = new Hono()

app.post('/', async (c) => {
const input = await c.req.json()
const post = await deps.posts.create(input)

return c.json({ data: post }, 201)
})

return app
}

业务规则放 service。

数据访问放 repository。

这样测试和替换实现都容易。


三、什么时候用 Monorepo

如果你只有一个后端项目,不需要 monorepo。

但如果你有:

1
2
3
4
5
6
web 前端
Hono API
共享类型
后台 worker
脚本工具
组件库

monorepo 会更舒服。

结构:

1
2
3
4
5
6
7
8
apps/
web/
api/
worker/
packages/
shared/
config/
api-client/

好处:

1
2
3
4
5
前后端共享类型
统一 lint/test/build
统一依赖版本
Hono RPC 类型更容易导出
重构跨应用更方便

缺点:

1
2
3
仓库变大
CI 需要做增量
依赖边界需要约束

不要为了看起来高级而上 monorepo。

但前后端都 TypeScript,而且接口类型共享强,monorepo 很适合 Hono。


四、共享类型怎么放

共享类型应该放在稳定包里:

1
2
3
4
5
packages/shared/src/
types/
constants/
errors/
schemas/

比如错误码:

1
2
3
4
5
6
7
export const ErrorCodes = {
AUTH_REQUIRED: 'AUTH_REQUIRED',
PERMISSION_DENIED: 'PERMISSION_DENIED',
POST_NOT_FOUND: 'POST_NOT_FOUND',
} as const

export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]

前端和后端都引用同一份。

不要前端手写一份:

1
2
3
if (error.code === 'POST_NOT_FOUND') {
// ...
}

后端又写另一份。

重复定义就是漂移的开始。


五、Hono RPC 在 monorepo 里很舒服

如果 API 和前端在同一仓库,可以导出 Hono app 类型:

1
2
3
4
5
6
// apps/api/src/app.ts
const routes = app
.route('/api/posts', postRoutes(deps))
.route('/api/auth', authRoutes(deps))

export type AppType = typeof routes

前端:

1
2
3
4
import { hc } from 'hono/client'
import type { AppType } from '@my-app/api'

export const api = hc<AppType>(import.meta.env.VITE_API_BASE_URL)

这样路径和参数都有类型提示。

但要注意:

1
只导出 type,不要让前端打包后端运行时代码

可以用 tsconfig、package exports、构建脚本来约束。

共享类型很香,但边界要守住。


六、CI 至少做四件事

一个 Hono 项目的 CI,不要只有 build。

至少:

1
2
3
4
typecheck
lint
test
build

package scripts:

1
2
3
4
5
6
7
8
9
{
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"build": "tsc",
"ci": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
}
}

如果项目暂时没有 lint,也至少要有:

1
2
3
typecheck
test
build

TypeScript 项目不跑 typecheck 就合并代码,有点浪费 TypeScript。


七、CI 里加接口 smoke test

构建成功不代表服务能用。

部署到预览环境后,最好跑几个 smoke test:

1
2
3
4
GET /health
GET /api/version
POST /api/auth/login with test account
GET /api/me

可以写一个简单脚本:

1
2
3
4
5
6
7
8
9
const baseUrl = process.env.API_BASE_URL!

const health = await fetch(`${baseUrl}/health`)

if (!health.ok) {
throw new Error(`health check failed: ${health.status}`)
}

console.log('smoke test passed')

CI 分两阶段:

1
2
代码阶段:typecheck/test/build
部署阶段:deploy preview + smoke test

这能挡住很多“构建成功但服务不可用”的问题。


八、环境分支和部署环境

不要把所有环境都混在一起。

常见模型:

1
2
3
feature branch -> preview
develop/main -> staging
tag/release -> production

或者简单一点:

1
2
main -> production
pull request -> preview

关键是:

1
2
3
4
每个环境有独立配置
每个环境有独立数据库或命名空间
生产 secret 不给 preview
preview 不写生产数据

Hono app 本身不复杂。

复杂的是它连接的资源。

CI/CD 要把这些环境边界守住。


九、脚手架要有 README

项目模板不是只给机器看的。

也要给人看。

README 至少写:

1
2
3
4
5
6
7
项目结构
本地启动
环境变量
数据库迁移
测试命令
部署命令
常见问题

比如:

1
2
3
4
5
6
7
## Local Development

1. pnpm install
2. cp .env.example .env
3. pnpm db:migrate
4. pnpm dev
5. pnpm test

新人能不能半小时跑起来,是工程质量的一个很真实指标。


十、不要过早模板化

有些团队一上来就做内部脚手架。

里面塞满:

1
2
3
4
5
6
7
8
9
认证
权限
日志
ORM
队列
缓存
OpenAPI
GraphQL
微服务

结果项目还没开始,模板已经很重。

我的建议:

1
第一版模板只放真正每个项目都需要的东西

比如:

1
2
3
4
5
6
7
Hono app factory
统一错误结构
requestId
config 校验
Vitest 示例
README
CI 基础流程

业务相关的东西不要急着塞进模板。

模板越轻,越容易被团队真正使用。


十一、推荐的最小 Hono 工程模板

我会这样开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src/
app.ts
index.ts
middleware/
request-id.ts
error-handler.ts
config/
runtime-config.ts
routes/
health.ts
errors/
app-error.ts
tests/
health.test.ts
.env.example
package.json
tsconfig.json
README.md

CI:

1
2
3
4
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build

然后随着业务增长,再加:

1
2
3
4
5
schemas
services
repositories
workers
packages/shared

结构要服务于复杂度。

不是一开始就把未来三年的复杂度摆在第一天。

Hono 的轻,应该体现在工程模板也轻。

但轻不等于散。

一个好的 Hono 模板,应该让团队快速开始,同时给后续增长留出清晰边界。