项目目录结构设计
要点
- 单文件路由超过 5 个接口后就会失控,拆分是必然的
- Hono 的
app.route()支持将路由拆分到独立文件,再在入口组装 routes/按领域组织,middleware/放中间件,helpers/放工具函数types/env.d.ts统一管理 Cloudflare Workers 的绑定类型wrangler.jsonc的 bindings 配置决定了运行时能力- 不同规模的项目用不同复杂度的结构,不要小项目套大架构
1. 从单文件到多文件
上一篇把所有路由写在一个 src/index.ts 里,四个接口还能应付。但真实项目不会停在四个接口。用户模块、聊天模块、健康检查、Webhook 回调——加到十几二十个路由时,一个文件会变成几百行的「上帝文件」,找路由靠滚动,改一处怕影响全局。
什么时候该拆分?实用的判断信号:
- 单个文件超过 150 行
- 路由涉及两个以上业务领域(用户、订单、消息……)
- 想加一个中间件但发现不知道插在哪
不用等到「必须拆」才动手。提前按领域分好文件,后续加路由时自然知道往哪放。
2. 推荐的目录结构
一个中型 Hono 项目的典型结构:
my-api/
src/
index.ts # 入口:创建 app,组装路由,导出
routes/ # 路由按领域拆分
health.ts # 健康检查 GET /api/health
users.ts # 用户模块 /api/users/*
chat.ts # 聊天模块 /api/chat/*
middleware/ # 自定义中间件
auth.ts # 鉴权:校验 token,注入用户信息
logger.ts # 日志:记录请求方法、路径、耗时
helpers/ # 工具函数
response.ts # 统一响应格式 { code, data, message }
types/ # 类型定义
env.d.ts # Cloudflare Workers 绑定类型
models.ts # 业务数据模型(User、Message 等)
wrangler.jsonc # Cloudflare Workers 配置
tsconfig.json # TypeScript 配置
.dev.vars # 本地环境变量(不进 git)
每个目录的边界:
- routes/ — 只放路由定义,一个文件对应一个业务领域
- middleware/ — 可复用的请求处理逻辑,比如鉴权、日志、CORS
- helpers/ — 纯函数工具,不依赖 Hono Context 的也可以放这里
- types/ — 类型定义集中管理,避免散落在各文件里重复声明
3. 路由拆分实践
上一篇的 src/index.ts 里有四个路由,包括 /api/health 和 /api/users/*。现在把它们拆到独立文件。
第一步,把用户相关的路由抽到 routes/users.ts:
// src/routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: `User ${id}` })
})
users.post('/', async (c) => {
const body = await c.req.json()
return c.json({ message: 'User created', data: body }, 201)
})
export default users注意两个关键点:
- 创建的是子 app(
const users = new Hono()),不是全局 app - 路由路径去掉了
/api/users前缀——前缀在挂载时加上
第二步,抽出健康检查:
// src/routes/health.ts
import { Hono } from 'hono'
const health = new Hono()
health.get('/', (c) => {
return c.json({ status: 'ok', timestamp: Date.now() })
})
export default health第三步,在入口文件用 app.route() 组装:
// src/index.ts
import { Hono } from 'hono'
import users from './routes/users'
import health from './routes/health'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
// 挂载子路由,第一个参数是路径前缀
app.route('/api/users', users)
app.route('/api/health', health)
export default appapp.route('/api/users', users) 把 users 子 app 挂载到 /api/users 前缀下。子 app 里的 /:id 变成 /api/users/:id,/ 变成 /api/users。最终路由表和拆分前完全一致,但代码分布在三个文件里,每个文件职责单一。
子 app 的 export 模式
上面的例子用 export default,适合一个文件导出一个子 app。如果需要在同一文件里导出多个路由组,可以用命名导出:
// src/routes/orders.ts
import { Hono } from 'hono'
export const orderRoutes = new Hono()
orderRoutes.get('/', (c) => c.json({ orders: [] }))
export const returnRoutes = new Hono()
returnRoutes.get('/', (c) => c.json({ returns: [] }))小项目用 default export 就够了,命名导出在路由组较多时更灵活。
4. 类型文件的组织
Cloudflare Workers 的绑定(D1、KV、R2 等)需要通过泛型注入到 Hono 的 Context 里。集中定义在 types/env.d.ts:
// src/types/env.d.ts
export type Env = {
Bindings: {
DB: D1Database // D1 数据库
CACHE: KVNamespace // KV 命名空间
API_SECRET: string // 环境变量
JWT_SECRET: string
}
}在路由和中间件里通过泛型引用:
// src/routes/users.ts
import { Hono } from 'hono'
import type { Env } from '../types/env'
const users = new Hono<Env>()
users.get('/:id', async (c) => {
const db = c.env.DB
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(c.req.param('id'))
.first()
return c.json(user)
})
export default users入口文件也加上泛型 new Hono<Env>(),这样 c.env.DB、c.env.CACHE 就有完整的类型提示,不用类型断言。
业务数据模型放 types/models.ts,集中管理的好处是改一处全局生效,不会出现两个文件对同一实体定义不一致的问题。
5. 配置文件的职责
wrangler.jsonc 的 bindings
上一篇介绍了 name、main、compatibility_date。真实项目还需要配置 bindings——Workers 访问外部能力的方式:
// wrangler.jsonc
{
"name": "my-api",
"main": "src/index.ts",
"compatibility_date": "2026-04-15",
// 静态环境变量(非敏感)
"vars": { "ENVIRONMENT": "production" },
// D1 数据库
"d1_databases": [{
"binding": "DB",
"database_name": "my-api-db",
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}],
// KV 键值存储
"kv_namespaces": [{
"binding": "CACHE",
"id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
}]
// r2_buckets、services 等同理
}每种 binding 的含义:
- vars:非敏感的环境变量,会随代码提交到 git
- d1_databases:绑定 D1 SQLite 数据库
- kv_namespaces:绑定 KV 键值存储,适合缓存
- r2_buckets:绑定 R2 对象存储,适合文件上传
- services:绑定其他 Worker 服务,实现服务间调用
binding 字段的值必须和 types/env.d.ts 里的属性名一致。binding: "DB" 对应代码里的 c.env.DB。
.dev.vars 本地环境变量
敏感信息(API Key、密钥)不要写在 wrangler.jsonc 里,放在 .dev.vars:
# .dev.vars(不要提交到 git)
API_SECRET=dev-only-secret-key
JWT_SECRET=dev-jwt-secretwrangler 在 wrangler dev 时自动加载这个文件。生产环境的同名变量在 Cloudflare Dashboard 配置。.dev.vars 必须加到 .gitignore。
tsconfig.json 关键配置
模板生成的配置通常不需要大改,但路径别名建议手动加上:
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"types": ["@cloudflare/workers-types"],
"paths": {
"@/*": ["./src/*"] // 路径别名
}
}
}项目文件层级变深时,@/types/env 比 ../../../types/env 清晰得多。
6. 不同规模项目的结构建议
不要小项目套大架构,结构复杂度应该跟着项目规模增长。
小型项目(1-5 个路由)
单文件就够了,不用拆。index.ts 没超过 150 行,一个文件能一眼看完。
中型项目(5-20 个路由)
就是第 2 节的完整结构,按领域拆分 routes,独立 middleware 和 helpers 目录。大多数项目停在这个阶段。
大型项目(20+ 路由,多团队协作)
在中型结构基础上增加分层:
src/
index.ts
routes/
users/
index.ts # 路由组装
handlers.ts # 处理函数
validators.ts # 参数校验
chat/
index.ts
handlers.ts
validators.ts
middleware/
services/ # 业务逻辑层,不依赖 Hono Context
repositories/ # 数据访问层,封装数据库操作
types/
路由处理函数只负责解析请求、调用 service、返回响应;业务逻辑在 service 里;数据库操作在 repository 里。但别急着跳到这个结构——等到路由文件超过 100 行、业务逻辑和 HTTP 处理混在一起难以测试时再拆。
7. 后续章节预告
目录结构是骨架,接下来的章节会逐个填充血肉:
- 路由系统:路径参数、通配符、路由分组、请求方法匹配
- 中间件:内置和自定义中间件的写法,执行顺序和组合方式
- 请求与响应:参数解析、Body 处理、文件上传、流式响应
- 错误处理:全局错误边界、自定义错误类、统一错误响应格式
建议现在就按第 2 节的结构整理项目,后续跟着做时不用回头调整。
延伸阅读
总结
项目目录结构不是一开始就要设计完美的——先用单文件跑起来,路由多了再按领域拆分。核心是理解 Hono 的 app.route() 机制:子 app 定义路由时不带公共前缀,入口文件挂载时统一加上。类型定义集中在 types/,bindings 类型和 wrangler.jsonc 保持一致。结构跟着项目长,不要提前过度设计。