Skip to main content

Path Parameters

Route parameters allow you to capture dynamic segments from the URL path using the :param syntax:
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.text(`User ID: ${id}`)
})

// GET /users/123 -> "User ID: 123"
// GET /users/john -> "User ID: john"

Getting All Parameters

You can retrieve all parameters at once as an object:
app.get('/posts/:id/comments/:commentId', (c) => {
  const { id, commentId } = c.req.param()
  return c.json({ postId: id, commentId })
})

// GET /posts/123/comments/456
// -> {"postId": "123", "commentId": "456"}
From src/request.ts:102, the param() method can be called with or without a key:
param(key?: string): unknown {
  return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams()
}

URL Decoding

Parameters are automatically URL-decoded:
app.get('/entry/:id', (c) => {
  const id = c.req.param('id')
  return c.text(`ID: ${id}`)
})

// GET /entry/%C3%A7awa%20y%C3%AE%3F
// -> "ID: çawa yî?"
The decoding logic in src/request.ts:109 handles this automatically:
#getDecodedParam(key: string): string | undefined {
  const paramKey = this.#matchResult[0][this.routeIndex][1][key]
  const param = this.#getParamValue(paramKey)
  return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param
}

Multiple Parameters

You can have multiple parameters in a single route:
app.get('/users/:userId/posts/:postId/comments/:commentId', (c) => {
  const { userId, userId, commentId } = c.req.param()
  return c.json({ userId, postId, commentId })
})

Optional Parameters

Mark parameters as optional by adding a ? after the segment:
app.get('/posts/:id/comments?', (c) => {
  return c.text('Post with optional comments path')
})

// GET /posts/123 -> Matches
// GET /posts/123/comments -> Matches
Optional parameters are handled by the router during route registration, creating multiple route entries.
The checkOptionalParameter utility in src/utils/url.ts handles this:
// From src/router/linear-router/router.ts:16
for (
  let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
  i < len;
  i++
) {
  this.#routes.push([method, paths[i], handler])
}

Pattern Constraints

Use regular expressions to constrain parameter values:
// Only match numeric IDs
app.get('/users/:id{[0-9]+}', (c) => {
  const id = c.req.param('id')
  return c.text(`User ID: ${id}`)
})

// GET /users/123 -> Matches
// GET /users/abc -> Does not match

// Date format
app.get('/date/:date{[0-9]{4}-[0-9]{2}-[0-9]{2}}', (c) => {
  const date = c.req.param('date')
  return c.text(`Date: ${date}`)
})

// GET /date/2024-01-15 -> Matches
// GET /date/invalid -> Does not match
From src/router/pattern-router/router.ts:24, patterns are converted to regex:
const match = part.match(/^\/:([^{]+)(?:{(.*)})?/)
return match
  ? `/(?<${match[1]}>${match[2] || '[^/]+'})`
  : part === '/*'
    ? '/[^/]+'
    : part.replace(/[.\\+*[^\]$()]/g, '\\$&')

Wildcard Parameters

The * wildcard matches any path segment(s):
// Match any subpath
app.get('/api/*', (c) => {
  return c.text('API route')
})

// GET /api/users -> Matches
// GET /api/users/123 -> Matches
// GET /api/v1/users/123 -> Matches

Combining Parameters with Wildcards

app.get('/users/:id/*', (c) => {
  const id = c.req.param('id')
  return c.text(`User ${id} subpath`)
})

// GET /users/123/profile -> "User 123 subpath"
// GET /users/123/settings/privacy -> "User 123 subpath"

Catch-All Routes

A single * catches all routes:
app.get('*', (c) => {
  return c.text('404 Not Found', 404)
})
Wildcard routes should typically be defined last, as they match broadly.

Parameter Matching Behavior

The router implementation determines how parameters are matched. From the router interface in src/router.ts:51:
export interface Router<T> {
  name: string
  add(method: string, path: string, handler: T): void
  match(method: string, path: string): Result<T>
}

Match Result Structure

The result of matching includes handlers and their parameter mappings:
// From src/router.ts:76
// Example result format:
[
  [
    [middlewareA, {}],                             // '*'
    [funcA,       {'id': '123'}],                  // '/user/:id/*'
    [funcB,       {'id': '123', 'action': 'abc'}], // '/user/:id/:action'
  ]
]

Router-Specific Behavior

Different routers handle parameters with varying performance characteristics:
  • RegExpRouter: Builds regex patterns for efficient matching
  • TrieRouter: Uses a trie data structure for prefix matching
  • LinearRouter: Iterates through routes sequentially
  • PatternRouter: Uses regex with named capture groups

Type Safety

With TypeScript, Hono provides type-safe parameter access:
const app = new Hono()

const route = app.get('/users/:id', (c) => {
  const id = c.req.param('id') // Type: string
  return c.json({ id })
})

// The route type includes parameter information

Best Practices

Choose parameter names that clearly indicate their purpose:
// Good
app.get('/users/:userId/posts/:postId', handler)

// Avoid
app.get('/users/:id/posts/:id2', handler)
Use pattern constraints or validation middleware:
app.get('/users/:id{[0-9]+}', (c) => {
  const id = c.req.param('id')
  // id is guaranteed to be numeric
  return c.json({ id: parseInt(id) })
})
Always check for undefined when accessing optional parameters:
app.get('/search', (c) => {
  const query = c.req.param('q') || 'default'
  return c.text(`Searching for: ${query}`)
})

Build docs developers (and LLMs) love