1. 基础路由方法
Hono 提供了与 HTTP 方法对应的路由注册方法。
每个方法接收路径和处理函数两个参数。
01import { Hono } from 'hono'0203const app = new Hono()0405// GET 请求 - 获取资源06app.get('/users', (c) => c.json({ users: [] }))0708// POST 请求 - 创建资源09app.post('/users', (c) => c.json({ message: 'created' }, 201))1011// PUT 请求 - 更新资源12app.put('/users/:id', (c) => c.json({ message: 'updated' }))1314// DELETE 请求 - 删除资源15app.delete('/users/:id', (c) => c.json({ message: 'deleted' }))1617// all - 匹配所有 HTTP 方法18app.all('/health', (c) => c.text('ok'))1920export default app
app.all() 会匹配任何 HTTP 方法,适合健康检查、CORS preflight 等不区分方法的场景。
2. 路由参数
用 :参数名 定义动态路径段,通过 c.req.param() 获取。
01import { Hono } from 'hono'0203const app = new Hono()0405// 单个参数06app.get('/users/:id', (c) => {07const id = c.req.param('id')08return c.json({ id })09})1011// 多个参数12app.get('/posts/:postId/comments/:commentId', (c) => {13const postId = c.req.param('postId')14const commentId = c.req.param('commentId')15return c.json({ postId, commentId })16})1718// 一次取出所有参数19app.get('/orgs/:orgId/teams/:teamId', (c) => {20const params = c.req.param()21// params: { orgId: '...', teamId: '...' }22return c.json(params)23})2425export default app
请求 GET /posts/42/comments/7 会返回 { postId: "42", commentId: "7" }。注意参数值始终是字符串,需要数字的话自己转换。
这里顺手区分一个新手常混淆的概念:
/users/42里的42是路径参数,用c.req.param('id')取/users?id=42里的42是查询参数,要用c.req.query('id')取
这一篇先把路由路径本身讲清楚,下一篇讲 Context 时再系统看请求数据的获取方式。
3. 可选参数与通配符
通配符 * 匹配路径中的剩余部分,适合文件路径、代理转发等场景。
01import { Hono } from 'hono'0203const app = new Hono()0405// 通配符 - 匹配 /files/ 后面的所有内容06// 用 :path{.+} 这种带命名的正则通配符,可以通过 c.req.param('path') 取到匹配的内容07app.get('/files/:path{.+}', (c) => {08const path = c.req.param('path')09// 请求 /files/docs/readme.md → path = 'docs/readme.md'10return c.json({ path })11})1213// 可选参数 - 用 ? 标记14app.get('/articles/:slug?', (c) => {15const slug = c.req.param('slug')16if (slug) {17return c.json({ article: slug })18}19return c.json({ articles: [] })20})2122export default app
/files/:path{.+} 会匹配 /files/a、/files/a/b/c 等任意深度的路径,param('path') 取到的值不包含前缀 /files/。如果只是"挡住一段路径"不需要取值,直接用 app.get('/files/*', ...) 也可以,只是没法通过 param 拿到通配符的匹配内容。
4. 分组路由 app.route()
真实项目不会把几十个路由全写在一个文件里。Hono 用 app.route() 把子路由挂载到主 app 上,每个模块独立管理自己的路由。
先定义各模块的路由:
1import { Hono } from 'hono'23const users = new Hono()45users.get('/', (c) => c.json({ users: [] }))6users.get('/:id', (c) => c.json({ id: c.req.param('id') }))7users.post('/', (c) => c.json({ message: 'user created' }, 201))89export default users
1import { Hono } from 'hono'23const posts = new Hono()45posts.get('/', (c) => c.json({ posts: [] }))6posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))78export default posts
然后在主入口挂载:
01import { Hono } from 'hono'02import users from './users'03import posts from './posts'0405const app = new Hono()0607app.route('/users', users)08app.route('/posts', posts)0910export default app
挂载后,users 里的 GET / 变成了 GET /users/,GET /:id 变成了 GET /users/:id。每个模块不需要知道自己最终挂在哪个前缀下。
这里有一个很重要的顺序问题:先把子模块里的路由定义完,再 app.route() 挂载到主 app 上。
也就是说,下面这种顺序是安全的:
- 在
users.ts里把users.get()、users.post()都写完 - 回到主入口执行
app.route('/users', users)
不要反过来写成“先挂载,再继续往 users 里追加路由”。对新手来说,这个坑很常见,一旦顺序写反,最后很可能会遇到明明写了路由却返回 404 的情况。
5. basePath 全局前缀
basePath() 给整个 app 的所有路由加上统一前缀,常用于 API 版本管理。
01import { Hono } from 'hono'0203const app = new Hono().basePath('/api/v1')0405app.get('/users', (c) => c.json({ users: [] }))06app.get('/posts', (c) => c.json({ posts: [] }))0708export default app09// 实际路由:10// GET /api/v1/users11// GET /api/v1/posts
basePath 和 app.route() 可以组合使用:
1import { Hono } from 'hono'2import users from './users'34const app = new Hono().basePath('/api/v1')5app.route('/users', users)67// users 模块的 GET / → GET /api/v1/users/8// users 模块的 GET /:id → GET /api/v1/users/:id9export default app
6. 路由优先级
Hono 的路由匹配遵循两条规则:
1. 精确路由优先于参数路由。
1import { Hono } from 'hono'23const app = new Hono()45app.get('/users/me', (c) => c.json({ name: '当前用户' }))6app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))78export default app
请求 GET /users/me 命中第一个路由,不会被 :id 捕获。请求 GET /users/42 命中第二个。
2. 同类型路由,先注册的优先。
1import { Hono } from 'hono'23const app = new Hono()45app.get('/users/:id', (c) => c.json({ handler: 'first' }))6app.get('/users/:userId', (c) => c.json({ handler: 'second' }))78// GET /users/42 → 命中第一个9export default app
这种写法没有意义,但说明了规则:两个参数路由结构相同时,先注册的赢。
不过在真实项目里,更常见的不是这个例子,而是兜底路由写得太早:
1import { Hono } from 'hono'23const app = new Hono()45app.get('/users/*', (c) => c.json({ handler: 'fallback' }))6app.get('/users/me', (c) => c.json({ handler: 'me' }))78export default app
如果你把更宽泛的匹配规则放在前面,后面的具体路由就可能没有机会执行。所以实战里的经验很简单:
- 越具体的路由,越靠前
- 越宽泛的兜底规则,越靠后
7. 实际项目的路由组织
一个典型的 Hono 项目按模块拆分路由文件,主入口只负责创建 app、挂载路由和全局中间件:
01import { Hono } from 'hono'02import { cors } from 'hono/cors'03import { logger } from 'hono/logger'04import users from './routes/users'05import posts from './routes/posts'06import auth from './routes/auth'0708const app = new Hono().basePath('/api/v1')0910// 全局中间件11app.use('*', cors())12app.use('*', logger())1314// 挂载路由模块15app.route('/users', users)16app.route('/posts', posts)17app.route('/auth', auth)1819export default app
01import { Hono } from 'hono'0203const app = new Hono()0405app.get('/', (c) => c.json({ users: [] }))06app.get('/:id', (c) => c.json({ id: c.req.param('id') }))07app.post('/', (c) => c.json({ message: 'created' }, 201))08app.put('/:id', (c) => c.json({ message: 'updated' }))09app.delete('/:id', (c) => c.json({ message: 'deleted' }))1011export default app
这个结构的好处:
- 每个模块文件只关心自己的业务路由,不关心前缀和全局中间件
- 主入口一目了然:用了什么中间件、挂了哪些模块、API 版本是什么
- 新增模块就是加一个文件 + 一行
app.route()
8. 总结
Hono 的路由系统和 Express 思路一致,但类型推导更完整。核心就这几件事:HTTP 方法对应 app.get/post/put/delete,动态参数用 :name,通配符用 *,模块拆分用 app.route(),全局前缀用 basePath()。
下一篇讲 Context 与请求响应——c 这个对象到底能干什么,怎么拿请求数据、怎么构造响应。