Hono 前后端分离系列 07:数据库访问和业务分层 Hono 很轻,但你的业务不会一直很轻。
很多 Hono 项目最开始都写得很舒服:
1 2 3 4 app.get ('/api/todos' , async (c) => { const rows = await db.select ().from (todos) return c.json ({ data : rows }) })
这没错。
但如果所有接口都直接在 route 里查数据库,项目很快会变成这样:
1 2 3 4 5 路由里有 SQL 路由里有权限判断 路由里有业务规则 路由里有数据转换 路由里还有错误处理
这种代码一开始快,后面难维护。
前后端分离项目里,Hono 最适合做 HTTP 层。数据库访问和业务规则,最好逐步拆出去。
一、先分清三层 我通常把 Hono API 分成三层:
1 2 3 route HTTP 输入输出 service 业务规则 repository 数据访问
route 负责:
service 负责:
权限判断
业务流程
事务边界
调用多个 repository
repository 负责:
这样拆的目的不是为了套架构,而是为了让每一层的变化范围变小。
二、一个用户模块示例 目录结构:
1 2 3 4 5 6 7 8 9 src/ routes/ users.ts services/ user-service.ts repositories/ user-repository.ts schemas/ user-schema.ts
route:
1 2 3 4 5 6 7 8 users.get ('/:id' , async (c) => { const id = c.req .param ('id' ) const user = await userService.getProfile (id) return c.json ({ data : user, }) })
service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export const userService = { async getProfile (id : string ) { const user = await userRepository.findById (id) if (!user) { throw new AppError ('USER_NOT_FOUND' , '用户不存在' , 404 ) } return { id : user.id , name : user.name , avatarUrl : user.avatar_url , } }, }
repository:
1 2 3 4 5 6 7 8 9 export const userRepository = { async findById (id : string ) { return db .selectFrom ('users' ) .select (['id' , 'name' , 'avatar_url' ]) .where ('id' , '=' , id) .executeTakeFirst () }, }
route 不关心数据库字段叫 avatar_url 还是 avatarUrl。
前端也不应该知道数据库字段名。
这就是 service 层的价值。
三、Cloudflare D1 的情况 如果你把 Hono 部署到 Cloudflare Workers,常见数据库是 D1。
Hono 的 Cloudflare Workers 文档提到,绑定可以通过 c.env 访问,并且可以用泛型给 binding 加类型。
比如:
1 2 3 4 5 type Bindings = { DB : D1Database } const app = new Hono <{ Bindings : Bindings }>()
route 里可以访问:
1 2 3 const result = await c.env .DB .prepare ('SELECT id, title FROM todos ORDER BY id DESC' ) .all ()
但不要每个 route 都直接写 SQL。
更好的做法是把 DB 传给 repository:
1 2 3 4 5 6 7 8 9 10 11 function createTodoRepository (db : D1Database ) { return { async list ( ) { const result = await db .prepare ('SELECT id, title, completed FROM todos ORDER BY id DESC' ) .all () return result.results }, } }
route:
1 2 3 4 5 6 todos.get ('/' , async (c) => { const repo = createTodoRepository (c.env .DB ) const items = await repo.list () return c.json ({ data : { items } }) })
后面要换 ORM、换数据库、加缓存,影响范围就更小。
四、事务要放在哪里 事务不应该散落在 route 里。
比如创建订单:
这是一个业务流程,应该属于 service。
伪代码:
1 2 3 4 5 6 7 8 9 10 export const orderService = { async createOrder (input : CreateOrderInput ) { return db.transaction ().execute (async (trx) => { const order = await orderRepository.create (trx, input) await inventoryRepository.decrease (trx, input.items ) await orderLogRepository.write (trx, order.id , 'created' ) return order }) }, }
不同数据库和 ORM 的事务 API 不一样,但原则一样:
1 2 3 route 不管理事务 service 管理业务事务 repository 接收事务上下文
这样以后排查问题时,你能很快知道业务一致性在哪里保证。
五、返回给前端的数据要做转换 数据库里的数据不一定适合直接给前端。
比如数据库字段:
1 2 3 4 created_at avatar_url password_hash deleted_at
前端需要:
1 2 3 4 5 6 { "id" : "u_1" , "name" : "Tom" , "avatarUrl" : "https://..." , "createdAt" : "2026-06-23T00:00:00.000Z" }
不要直接:
1 return c.json ({ data : row })
至少要做 DTO 转换:
1 2 3 4 5 6 7 8 function toUserDTO (user : UserRow ) { return { id : user.id , name : user.name , avatarUrl : user.avatar_url , createdAt : user.created_at , } }
这也是安全边界。
你不会不小心把 password_hash 返回给前端。
六、service 不要依赖 Hono Context 一个重要原则:
不要这样写:
1 2 3 4 async function createUser (c : Context ) { const body = await c.req .json () const db = c.env .DB }
这会把业务逻辑绑死在 Hono。
更推荐:
1 2 3 async function createUser (deps : Deps , input : CreateUserInput ) { return deps.userRepository .create (input) }
route 负责把 Hono 的 c 转成 service 需要的依赖和输入。
这样 service 更容易测试,也更容易迁移。
七、最后的建议 Hono 可以让 API 层非常轻。
但轻不等于所有代码都写在 route 里。
我的建议是:
1 2 3 4 5 6 7 小项目可以先 route + service 有数据库后尽早拆 repository route 只处理 HTTP service 处理业务规则 repository 处理数据访问 不要把数据库字段直接返回给前端 service 不要依赖 Hono Context
前后端分离做得稳,后端分层也要稳。
否则前端看起来分离了,后端内部还是一团糊。
参考资料