Hono 前后端分离系列 07:数据库访问和业务分层
wxk1991 Lv5

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
  • 返回响应

service 负责:

  • 权限判断
  • 业务流程
  • 事务边界
  • 调用多个 repository

repository 负责:

  • 查询数据库
  • 写入数据库
  • 封装 SQL 或 ORM

这样拆的目的不是为了套架构,而是为了让每一层的变化范围变小。


二、一个用户模块示例

目录结构:

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 里。

比如创建订单:

1
2
3
4
创建订单
扣库存
写订单日志
发送通知

这是一个业务流程,应该属于 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
service 层不要直接依赖 c

不要这样写:

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

前后端分离做得稳,后端分层也要稳。

否则前端看起来分离了,后端内部还是一团糊。


参考资料