Skip to content

Trong Hono, tại sao không nên tách Route và Controller như MVC truyền thống?

Hono khuyến nghị viết handler trực tiếp ngay sau định nghĩa route — không tách Controller. Cách này tận dụng toàn bộ khả năng type inference của TypeScript.
Nếu bạn từng làm việc với Express.js hoặc các framework MVC như Laravel, Ruby on Rails, bạn chắc chắn quen với pattern này: tách bạch Route và Controller thành hai file riêng biệt.
routes/
books.route.ts
controllers/
books.controller.ts
Trông có vẻ gọn gàng, có tổ chức. Nhưng khi bạn mang tư duy này sang Hono — đặc biệt nếu dùng TypeScript — bạn sẽ gặp ngay một vấn đề khá khó chịu.

Vấn đề với Controller kiểu MVC trong Hono

Giả sử bạn đang xây dựng một API quản lý sách. Theo kiểu MVC quen thuộc, bạn sẽ viết Controller như này:
// 🙁 books.controller.ts
import { Context } from 'hono'

export const getBook = (c: Context) => {
const id = c.req.param('id') // ❌ TypeScript không thể infer ':id' ở đây
return c.json(`get book ${id}`)
}
Rồi ở file route:
// books.route.ts
import { Hono } from 'hono'
import { getBook } from './books.controller'

const app = new Hono()
app.get('/books/:id', getBook)

export default app
Nhìn code chạy vẫn đúng, nhưng TypeScript sẽ không thể biết rằng c.req.param('id') là hợp lệ. Type của c lúc này chỉ là Context generic — không mang theo thông tin về path parameter :id.
Nếu bạn đổi tên param trên route thành :bookId mà quên cập nhật trong Controller, TypeScript sẽ không báo lỗi. Bug âm thầm xuất hiện mà không ai hay.
Để fix điều này, bạn phải viết generic phức tạp:
// 😰 Phải viết thế này mới đúng type
const getBook = (c: Context<{ Bindings: {}; Variables: {} }, '/books/:id'>) => {
...
}
Rõ ràng là không thoải mái chút nào.

Hono khuyến nghị gì?

Hono khuyến nghị viết handler trực tiếp ngay sau định nghĩa route — không tách Controller. Cách này tận dụng toàn bộ khả năng type inference của TypeScript:
// 😃 books.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/books/:id', (c) => {
const id = c.req.param('id') // ✅ TypeScript infer đúng, có autocomplete
return c.json(`get book ${id}`)
})

export default app
Vì handler được viết ngay tại chỗ định nghĩa route, Hono biết chính xác path pattern là gì, từ đó infer được kiểu dữ liệu của c.req.param() một cách tự động. Không cần generic phức tạp, không lo bug âm thầm.

Vậy tổ chức code lớn hơn thì sao?

Không có nghĩa là bạn phải nhét tất cả vào một file index.ts khổng lồ. Hono cung cấp app.route() để gom nhóm các endpoint theo tài nguyên, mỗi tài nguyên là một file riêng.

Ví dụ: API có cả /authors/books

// authors.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list authors'))
app.post('/', (c) => c.json('create an author', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
// books.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'

const app = new Hono()

app.route('/authors', authors)
app.route('/books', books)

export default app
Cấu trúc thư mục lúc này sẽ là:
src/
index.ts
authors.ts
books.ts
Mỗi file là một mini-app Hono độc lập, chứa cả route lẫn handler. Gọn, rõ ràng, dễ maintain.

Nếu vẫn muốn tách handler ra — dùng factory.createHandlers()

Đôi khi bạn có lý do chính đáng để tách handler ra file riêng: tái sử dụng ở nhiều route, hoặc file route đang quá dài và muốn chia nhỏ. Hono có factory.createHandlers() trong hono/factory để làm điều này mà vẫn giữ được type inference đầy đủ.

Cách hoạt động

createFactory() tạo ra một factory object. Từ factory đó, bạn dùng factory.createHandlers() để định nghĩa một mảng handler — bao gồm middleware và handler cuối cùng. Khi gắn vào route, bạn spread mảng đó ra.
import { createFactory } from 'hono/factory'

const factory = createFactory()

const handlers = factory.createHandlers((c) => {
return c.json('ok')
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.