基础路由编写

要点

  • 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 app

app.get(path, handler) 的第一个参数是路径字符串,第二个参数是处理函数(handler)。

handler 的签名是:

// handler 签名
(c: Context) => Response | Promise<Response>

c 是 Context 对象,封装了请求和响应的所有操作。常用方法:

  • c.text('...') — 返回纯文本响应
  • c.json(&#123;...&#125;) — 返回 JSON 响应
  • c.html('...') — 返回 HTML 响应
  • c.json(&#123;...&#125;, 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/42c.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/200userId'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 app

app.route('/api/users', users) 的含义:所有访问 /api/users 开头的请求,交给 users 这个子应用处理。子应用内部的路径是相对前缀的,所以 users.get('/') 实际上匹配 /api/usersusers.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,第一个路由命中,返回 &#123; type: 'current user' &#125;。 访问 /api/users/42,第一个不匹配,走第二个路由,返回 &#123; type: 'user by id', id: '42' &#125;

静态路径 /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 的中间件系统,看看如何在请求处理流程中插入鉴权、日志、错误处理等通用逻辑。