Skip to Content

Last Updated: 3/9/2026


Routing

Routing is how Hono maps URLs to handlers. Hono’s routing is flexible, intuitive, and blazing fast.

Basic Routes

Define routes using HTTP method functions:

import { Hono } from 'hono' const app = new Hono() // GET request app.get('/', (c) => c.text('GET /')) // POST request app.post('/', (c) => c.text('POST /')) // PUT request app.put('/', (c) => c.text('PUT /')) // DELETE request app.delete('/', (c) => c.text('DELETE /'))

All HTTP methods are supported:

  • app.get() - GET
  • app.post() - POST
  • app.put() - PUT
  • app.delete() - DELETE
  • app.patch() - PATCH
  • app.head() - HEAD
  • app.options() - OPTIONS

Any Method

Handle all HTTP methods for a route:

app.all('/hello', (c) => c.text('Any Method /hello'))

Custom Methods

Handle custom HTTP methods:

// Single custom method app.on('PURGE', '/cache', (c) => c.text('PURGE Method /cache')) // Multiple methods app.on(['PUT', 'DELETE'], '/post', (c) => c.text('PUT or DELETE /post') )

Multiple Paths

Register the same handler for multiple paths:

app.on('GET', ['/hello', '/ja/hello', '/en/hello'], (c) => c.text('Hello') )

Path Parameters

Basic Parameters

Capture dynamic segments in URLs:

app.get('/user/:name', (c) => { const name = c.req.param('name') return c.text(`Hello, ${name}!`) })
GET /user/alice → "Hello, alice!" GET /user/bob → "Hello, bob!"

Multiple Parameters

Get all parameters at once:

app.get('/posts/:id/comment/:commentId', (c) => { const { id, commentId } = c.req.param() return c.json({ postId: id, commentId }) })
GET /posts/123/comment/456 → { "postId": "123", "commentId": "456" }

Type safety: Parameter names are inferred as literal types:

app.get('/posts/:id', (c) => { const id = c.req.param('id') // ✅ Works const slug = c.req.param('slug') // ❌ Type error! })

Optional Parameters

Make parameters optional with ?:

// Matches both /api/animal and /api/animal/dog app.get('/api/animal/:type?', (c) => { const type = c.req.param('type') return c.text(`Animal: ${type || 'any'}`) })
GET /api/animal → "Animal: any" GET /api/animal/dog → "Animal: dog"

Pattern Matching

Regular Expressions

Constrain parameters with regex:

// Only match numeric IDs app.get('/post/:id{[0-9]+}', (c) => { const id = c.req.param('id') return c.text(`Post ${id}`) })
GET /post/123 → Matches GET /post/abc → Does not match

Multiple constraints:

app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => { const { date, title } = c.req.param() return c.json({ date, title }) })
GET /post/20240101/hello → Matches GET /post/abc/hello → Does not match GET /post/20240101/Hello → Does not match (uppercase)

Including Slashes

Capture paths including slashes:

// Matches any path ending in .png app.get('/posts/:filename{.+\\.png}', (c) => { const filename = c.req.param('filename') return c.text(`Image: ${filename}`) })
GET /posts/photo.png → Matches GET /posts/2024/01/photo.png → Matches GET /posts/2024/01/photo.jpg → Does not match

Wildcards

Use * to match any segment:

app.get('/wild/*/card', (c) => { return c.text('Wildcard matched') })
GET /wild/any/card → Matches GET /wild/thing/card → Matches GET /wild/any/thing/card → Does not match (multiple segments)

Match everything after a prefix:

app.get('/api/*', (c) => { return c.text('API endpoint') })
GET /api/users → Matches GET /api/posts/123 → Matches GET /api/v1/users/456 → Matches

Chained Routes

Chain multiple methods on the same path:

app .get('/endpoint', (c) => c.text('GET /endpoint')) .post((c) => c.text('POST /endpoint')) .delete((c) => c.text('DELETE /endpoint'))

This is equivalent to:

app.get('/endpoint', (c) => c.text('GET /endpoint')) app.post('/endpoint', (c) => c.text('POST /endpoint')) app.delete('/endpoint', (c) => c.text('DELETE /endpoint'))

Grouping Routes

With Hono Instances

Organize routes into separate modules:

// books.ts import { Hono } from 'hono' const books = new Hono() books.get('/', (c) => c.text('List Books')) // GET /book books.get('/:id', (c) => { // GET /book/:id const id = c.req.param('id') return c.text(`Get Book: ${id}`) }) books.post('/', (c) => c.text('Create Book')) // POST /book export default books
// app.ts import { Hono } from 'hono' import books from './books' const app = new Hono() app.route('/book', books) export default app

Without Changing Base

Group multiple instances while keeping their base paths:

const books = new Hono() books.get('/book', (c) => c.text('List Books')) // GET /book books.post('/book', (c) => c.text('Create Book')) // POST /book const users = new Hono().basePath('/user') users.get('/', (c) => c.text('List Users')) // GET /user users.post('/', (c) => c.text('Create User')) // POST /user const app = new Hono() app.route('/', books) // Handles /book app.route('/', users) // Handles /user

Base Path

Set a base path for all routes:

const api = new Hono().basePath('/api') api.get('/book', (c) => c.text('List Books')) // GET /api/book api.get('/user', (c) => c.text('List Users')) // GET /api/user

Routing Priority

Handlers execute in registration order:

app.get('/book/a', (c) => c.text('a')) app.get('/book/:slug', (c) => c.text('common'))
GET /book/a → "a" GET /book/b → "common"

Important: Once a handler returns, processing stops:

app.get('*', (c) => c.text('common')) // This runs first app.get('/foo', (c) => c.text('foo')) // This never runs!
GET /foo → "common" (foo handler is never reached)

Middleware Before Handlers

Middleware should be registered before handlers:

import { logger } from 'hono/logger' // ✅ Correct order app.use(logger()) // Middleware first app.get('/foo', (c) => c.text('foo')) // Then handlers

Fallback Handlers

Register fallback handlers after specific routes:

app.get('/bar', (c) => c.text('bar')) // Specific route first app.get('*', (c) => c.text('fallback')) // Fallback last
GET /bar → "bar" GET /foo → "fallback"

Hostname Routing

With Full URL

Route based on hostname in the URL:

const app = new Hono({ getPath: (req) => req.url.replace(/^https?:\/\/([^?]+).*$/, '$1'), }) app.get('/www1.example.com/hello', (c) => c.text('hello www1')) app.get('/www2.example.com/hello', (c) => c.text('hello www2'))

With Host Header

Route based on the Host header:

const app = new Hono({ getPath: (req) => '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/.*)/, '$1'), }) app.get('/www1.example.com/hello', (c) => c.text('hello www1')) // Matches requests with Host: www1.example.com header

Use cases:

  • Multi-tenant applications
  • Different behavior per subdomain
  • User-Agent based routing (using same technique with User-Agent header)

Strict Mode

By default, Hono distinguishes trailing slashes:

const app = new Hono() // strict mode ON by default app.get('/hello', (c) => c.text('Hello'))
GET /hello → Matches GET /hello/ → Does not match

Disable strict mode to treat them equally:

const app = new Hono({ strict: false }) app.get('/hello', (c) => c.text('Hello'))
GET /hello → Matches GET /hello/ → Also matches

Router Selection

Hono uses SmartRouter by default, which automatically chooses the best router. You can manually specify a router:

import { Hono } from 'hono' import { RegExpRouter } from 'hono/router/reg-exp-router' import { TrieRouter } from 'hono/router/trie-router' // Use RegExpRouter explicitly const app = new Hono({ router: new RegExpRouter() }) // Or TrieRouter const app2 = new Hono({ router: new TrieRouter() })

When to specify:

  • Usually not needed - SmartRouter picks the best one
  • Use LinearRouter for serverless with frequent cold starts
  • Use PatternRouter for smallest possible bundle size

See also: Router Internals for performance details.

Common Patterns

REST API

const api = new Hono() // List resources api.get('/posts', (c) => c.json({ posts: [] })) // Get single resource api.get('/posts/:id', (c) => { const id = c.req.param('id') return c.json({ post: { id } }) }) // Create resource api.post('/posts', async (c) => { const body = await c.req.json() return c.json({ post: body }, 201) }) // Update resource api.put('/posts/:id', async (c) => { const id = c.req.param('id') const body = await c.req.json() return c.json({ post: { id, ...body } }) }) // Delete resource api.delete('/posts/:id', (c) => { const id = c.req.param('id') return c.json({ deleted: id }) })

Versioned API

const v1 = new Hono() v1.get('/users', (c) => c.json({ version: 'v1' })) const v2 = new Hono() v2.get('/users', (c) => c.json({ version: 'v2' })) const app = new Hono() app.route('/api/v1', v1) app.route('/api/v2', v2)

Nested Resources

app.get('/posts/:postId/comments', (c) => { const postId = c.req.param('postId') return c.json({ comments: [], postId }) }) app.get('/posts/:postId/comments/:commentId', (c) => { const { postId, commentId } = c.req.param() return c.json({ comment: { postId, commentId } }) })

What’s Next?