基础路由编写
要点
- Hono 支持 GET、POST、PUT、DELETE、PATCH 等标准 HTTP 方法
- 路径参数用
:name声明,通过c.req.param()读取 app.route()实现路由前缀拆分,便于多文件组织- 静态路径优先级高于动态路径;注册顺序影响最终匹配结果
app.all()匹配所有 HTTP 方法,app.on()指定方法匹配- 链式写法让路由注册更紧凑
app.notFound()自定义 404 响应
1. 路由的基本语法
Hono 路由注册的基本形式:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.post('/api/users', async (c) => {
const body = await c.req.json()
return c.json({ message: 'Created', data: body }, 201)
})
export default appapp.get(path, handler) 的第一个参数是路径字符串,第二个参数是处理函数(handler)。
handler 的签名是:
// handler 签名
(c: Context) => Response | Promise<Response>c 是 Context 对象,封装了请求和响应的所有操作。常用方法:
c.text('...')— 返回纯文本响应c.json({...})— 返回 JSON 响应c.html('...')— 返回 HTML 响应c.json({...}, 201)— 第二个参数指定 HTTP 状态码
一个 handler 只能返回一个 Response。如果需要条件分支,在 handler 内部做判断,最终返回一个结果:
// src/index.ts
app.get('/api/greeting', (c) => {
const name = c.req.query('name')
if (!name) {
return c.json({ error: 'name is required' }, 400)
}
return c.json({ message: `Hello, ${name}!` })
})支持的 HTTP 方法:
// src/index.ts
app.get('/resource', (c) => { /* 获取资源 */ })
app.post('/resource', (c) => { /* 创建资源 */ })
app.put('/resource/:id', (c) => { /* 全量更新资源 */ })
app.delete('/resource/:id', (c) => { /* 删除资源 */ })
app.patch('/resource/:id', (c) => { /* 部分更新资源 */ })2. 路径参数(Path Parameters)
路径参数用于从 URL 中提取动态片段,用 :name 声明:
// src/index.ts
app.get('/api/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})访问 /api/users/42,c.req.param('id') 返回 '42'。
多个路径参数:
// src/index.ts
app.get('/api/users/:userId/posts/:postId', (c) => {
const userId = c.req.param('userId')
const postId = c.req.param('postId')
return c.json({ userId, postId })
})访问 /api/users/10/posts/200,userId 是 '10',postId 是 '200'。
一次性获取所有路径参数:
// src/index.ts
app.get('/api/users/:userId/posts/:postId', (c) => {
const params = c.req.param()
// params = { userId: '10', postId: '200' }
return c.json(params)
})注意:c.req.param() 返回的所有值都是字符串类型,如果需要数字要做转换:
// src/index.ts
app.get('/api/users/:id', (c) => {
const id = Number(c.req.param('id'))
if (Number.isNaN(id)) {
return c.json({ error: 'id must be a number' }, 400)
}
return c.json({ id })
})3. 通配符路由
星号 * 作为通配符,匹配任意路径段:
// src/index.ts
// 匹配 /api/ 下的所有路径
app.get('/api/*', (c) => {
return c.json({ message: 'API route matched' })
})常见用途是中间件和 404 兜底。
所有 API 请求统一鉴权:
// src/index.ts
app.use('/api/*', async (c, next) => {
const auth = c.req.header('Authorization')
if (!auth) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
})捕获所有未匹配的路径,返回自定义 404:
// src/index.ts
app.get('*', (c) => {
return c.json({ error: 'Route not found' }, 404)
})通配符也可以出现在路径中间:
// src/index.ts
// 匹配 /files/ 下任意深度的路径
app.get('/files/*/download', (c) => {
return c.text('Downloading...')
})4. 路由分组(app.route)
当路由越来越多,全写在一个文件里会变得难以维护。app.route() 可以把路由拆分到多个文件:
// src/routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/', (c) => {
return c.json({ users: [] })
})
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: 'Created', data: body }, 201)
})
export default users// src/routes/posts.ts
import { Hono } from 'hono'
const posts = new Hono()
posts.get('/', (c) => {
return c.json({ posts: [] })
})
posts.get('/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, title: `Post ${id}` })
})
export default posts// src/index.ts
import { Hono } from 'hono'
import users from './routes/users'
import posts from './routes/posts'
const app = new Hono()
app.route('/api/users', users)
app.route('/api/posts', posts)
export default appapp.route('/api/users', users) 的含义:所有访问 /api/users 开头的请求,交给 users 这个子应用处理。子应用内部的路径是相对前缀的,所以 users.get('/') 实际上匹配 /api/users,users.get('/:id') 匹配 /api/users/:id。
这种拆分方式的好处:
- 每个路由文件职责清晰
- 子应用可以独立测试
- 前缀集中管理,改一处生效
5. 路由优先级与匹配规则
当一个请求可能匹配多个路由时,Hono 按以下规则决定谁来处理:
静态路径优先于动态路径。
// src/index.ts
app.get('/api/users/me', (c) => {
return c.json({ type: 'current user' })
})
app.get('/api/users/:id', (c) => {
return c.json({ type: 'user by id', id: c.req.param('id') })
})访问 /api/users/me,第一个路由命中,返回 { type: 'current user' }。
访问 /api/users/42,第一个不匹配,走第二个路由,返回 { type: 'user by id', id: '42' }。
静态路径 /me 比动态路径 /:id 优先级高,不管注册顺序如何。
同优先级的路由按注册顺序匹配,先注册的先匹配。
// src/index.ts
app.get('/api/data', (c) => {
return c.text('First handler')
})
app.get('/api/data', (c) => {
return c.text('Second handler')
})两个路由路径完全相同,访问 /api/data,第一个 handler 返回响应,第二个永远不会执行。
常见陷阱:通配符注册顺序不当。
// ❌ 错误示例
app.get('/api/*', (c) => {
return c.text('Catch all')
})
app.get('/api/health', (c) => {
return c.text('OK')
})/api/health 的请求会被第一个路由拦截,因为 /api/* 先注册且能匹配。正确做法是把通配符放最后:
// ✅ 正确示例
app.get('/api/health', (c) => {
return c.text('OK')
})
app.get('/api/*', (c) => {
return c.text('Catch all')
})6. all() 和 on() 方法
all() — 匹配所有 HTTP 方法
// src/index.ts
app.all('/api/echo', async (c) => {
const method = c.req.method
return c.json({ method, message: `Echo from ${method}` })
})不管 GET、POST、PUT、DELETE 哪个方法访问 /api/echo,都会命中这个 handler。
典型用途是处理 CORS 预检请求:
// src/index.ts
app.all('/api/*', async (c) => {
if (c.req.method === 'OPTIONS') {
return c.text('', 204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
})
}
await next()
})on() — 指定方法匹配
on() 可以一次指定多个方法:
// src/index.ts
app.on('GET', '/api/resource', (c) => {
return c.json({ action: 'list' })
})
app.on(['POST', 'PUT'], '/api/resource', async (c) => {
const body = await c.req.json()
return c.json({ action: 'create or update', data: body })
})on() 在需要精确控制方法组合时比较方便,但大多数情况下直接用 app.get()、app.post() 更清晰。
7. 路由链式写法
Hono 支持链式注册路由,app.get() 等方法返回 app 实例本身:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
.get('/', (c) => c.text('Home'))
.get('/api/health', (c) => c.json({ status: 'ok' }))
.post('/api/users', async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
.get('/api/users/:id', (c) => {
return c.json({ id: c.req.param('id') })
})
export default app链式写法紧凑,适合路由较少、逻辑简单的场景。路由多了之后,链式反而不如独立语句清晰。按团队习惯选择即可。
也可以在同一路径上链多个中间件:
// src/index.ts
app.get('/api/protected',
async (c, next) => {
// 中间件 1:鉴权
const auth = c.req.header('Authorization')
if (!auth) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
},
async (c, next) => {
// 中间件 2:日志
console.log(`${c.req.method} ${c.req.path}`)
await next()
},
(c) => {
// handler
return c.json({ message: 'Protected data' })
}
)8. 自定义 404 处理
当请求没有匹配到任何路由时,Hono 默认返回 404 Not Found。可以用 app.notFound() 自定义响应:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/health', (c) => {
return c.json({ status: 'ok' })
})
// 自定义 404
app.notFound((c) => {
return c.json({
error: 'Not Found',
message: `Route ${c.req.path} does not exist`,
}, 404)
})
export default app访问一个不存在的路径,比如 /api/unknown,会返回:
{
"error": "Not Found",
"message": "Route /api/unknown does not exist"
}app.notFound() 接收的 handler 签名和普通路由一样,(c: Context) => Response,可以在里面做任何逻辑——比如根据 Accept 头返回 HTML 或 JSON 格式的 404 页面。
延伸阅读
总结
Hono 的路由系统覆盖了常见场景:标准 HTTP 方法、路径参数、通配符、路由分组、优先级规则、all() 全方法匹配、链式写法、自定义 404。
几个关键记忆点:
- 路径参数用
:name,读取用c.req.param('name') - 路由拆分用
app.route(prefix, subApp),子应用内部路径相对前缀 - 静态路径优先级高于动态路径;通配符放最后注册
app.notFound()自定义 404 响应格式
下一篇会讲 Hono 的中间件系统,看看如何在请求处理流程中插入鉴权、日志、错误处理等通用逻辑。