Last Updated: 3/9/2026
Context and Handlers
Every request in Hono goes through a handler function that receives a Context object. Understanding the Context is key to working with Hono.
The Context Object
The Context (c) is your interface to the current request and response. It’s created for each request and contains everything you need:
app.get('/hello', (c) => {
// `c` is the Context object
// It has the request, response helpers, and more
return c.text('Hello!')
})What’s in the Context?
c.req- The request objectc.res- The response object (Web Standard Response)c.env- Environment variables and bindingsc.executionCtx- Execution context (Cloudflare Workers)- Helper methods for responses (
c.text(),c.json(), etc.) - Methods to get/set values (
c.get(),c.set())
Reading Requests
URL Parameters
Get path parameters from the URL:
app.get('/posts/:id', (c) => {
const id = c.req.param('id')
return c.text(`Post ID: ${id}`)
})Get all parameters:
app.get('/posts/:id/comments/:commentId', (c) => {
const { id, commentId } = c.req.param()
return c.json({ postId: id, commentId })
})Query Parameters
Get query string values:
app.get('/search', (c) => {
const query = c.req.query('q')
const page = c.req.query('page')
return c.json({ query, page })
})GET /search?q=hono&page=2
→ { "query": "hono", "page": "2" }Get all query parameters:
app.get('/search', (c) => {
const params = c.req.query()
return c.json(params)
})Headers
Read request headers:
app.get('/', (c) => {
const userAgent = c.req.header('User-Agent')
const auth = c.req.header('Authorization')
return c.text(`User-Agent: ${userAgent}`)
})Get all headers:
app.get('/', (c) => {
const headers = c.req.header()
return c.json(headers)
})Request Body
JSON body:
app.post('/api/posts', async (c) => {
const body = await c.req.json()
return c.json({ received: body })
})Form data:
app.post('/submit', async (c) => {
const formData = await c.req.parseBody()
return c.json(formData)
})Text body:
app.post('/webhook', async (c) => {
const text = await c.req.text()
return c.text(`Received: ${text}`)
})Array buffer:
app.post('/upload', async (c) => {
const buffer = await c.req.arrayBuffer()
return c.text(`Received ${buffer.byteLength} bytes`)
})Blob:
app.post('/upload', async (c) => {
const blob = await c.req.blob()
return c.text(`Received ${blob.size} bytes`)
})Raw Request
Access the underlying Web Standard Request:
app.get('/', (c) => {
const request = c.req.raw
// This is a standard Request object
console.log(request.url)
console.log(request.method)
return c.text('OK')
})Sending Responses
Text Response
app.get('/', (c) => {
return c.text('Hello Hono!')
})With status code:
app.post('/posts', (c) => {
return c.text('Created!', 201)
})JSON Response
app.get('/api/posts', (c) => {
return c.json({
posts: [
{ id: 1, title: 'Hello' },
{ id: 2, title: 'World' }
]
})
})With status code:
app.post('/api/posts', (c) => {
return c.json({ message: 'Created' }, 201)
})HTML Response
app.get('/', (c) => {
return c.html('<h1>Hello Hono!</h1>')
})With JSX:
app.get('/', (c) => {
return c.html(
<html>
<body>
<h1>Hello Hono!</h1>
</body>
</html>
)
})See also: JSX Templating Guide
Redirect
app.get('/old-path', (c) => {
return c.redirect('/new-path')
})Permanent redirect (301):
app.get('/old-path', (c) => {
return c.redirect('/new-path', 301)
})Not Found
app.get('/posts/:id', (c) => {
const post = findPost(c.req.param('id'))
if (!post) {
return c.notFound()
}
return c.json({ post })
})Custom Response
Return any Web Standard Response:
app.get('/', (c) => {
return new Response('Custom response', {
status: 200,
headers: {
'Content-Type': 'text/plain',
'X-Custom-Header': 'value'
}
})
})Or use c.body() for more control:
app.get('/', (c) => {
return c.body('Response body', 200, {
'Content-Type': 'text/plain',
'X-Custom-Header': 'value'
})
})Setting Status and Headers
Status Code
app.post('/posts', (c) => {
c.status(201) // Set status code
return c.text('Created!')
})Or inline:
app.post('/posts', (c) => {
return c.text('Created!', 201)
})Response Headers
app.get('/', (c) => {
c.header('X-Message', 'Hello')
c.header('X-Request-ID', '123')
return c.text('OK')
})Or inline:
app.get('/', (c) => {
return c.text('OK', 200, {
'X-Message': 'Hello',
'X-Request-ID': '123'
})
})Accessing Response
Modify the response object directly:
app.use(async (c, next) => {
await next()
c.res.headers.append('X-Debug', 'Debug message')
})Storing and Retrieving Values
Set and Get
Share data between middleware and handlers:
// Middleware sets a value
app.use(async (c, next) => {
c.set('message', 'Hono is cool!!')
await next()
})
// Handler gets the value
app.get('/', (c) => {
const message = c.get('message')
return c.text(message)
})Type-safe with generics:
type Variables = {
message: string
userId: number
}
const app = new Hono<{ Variables: Variables }>()
app.use(async (c, next) => {
c.set('message', 'Hello') // Type-checked
c.set('userId', 123) // Type-checked
await next()
})
app.get('/', (c) => {
const message = c.get('message') // string
const userId = c.get('userId') // number
return c.json({ message, userId })
})Using c.var
Access variables with the c.var shorthand:
type Variables = {
user: { id: number; name: string }
}
const app = new Hono<{ Variables: Variables }>()
app.use(async (c, next) => {
c.set('user', { id: 1, name: 'Alice' })
await next()
})
app.get('/', (c) => {
const user = c.var.user // Type-safe access
return c.json({ user })
})Environment Variables
Cloudflare Workers
Access bindings (KV, D1, R2, secrets):
type Bindings = {
DB: D1Database
BUCKET: R2Bucket
API_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/data', async (c) => {
// Access D1 database
const result = await c.env.DB
.prepare('SELECT * FROM users')
.all()
// Access R2 bucket
const file = await c.env.BUCKET.get('file.txt')
// Access secret
const apiKey = c.env.API_KEY
return c.json({ result })
})Other Runtimes
Access process.env or Deno.env:
app.get('/', (c) => {
// Node.js / Bun
const apiKey = process.env.API_KEY
// Deno
const apiKey = Deno.env.get('API_KEY')
return c.text('OK')
})Execution Context
Cloudflare Workers
Access the execution context for waitUntil:
app.get('/track', async (c) => {
// Don't wait for analytics
c.executionCtx.waitUntil(
fetch('https://analytics.example.com', {
method: 'POST',
body: JSON.stringify({ event: 'page_view' })
})
)
return c.text('Tracked!')
})Error Handling
Access errors in middleware:
app.use(async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
}
})Handle errors globally:
app.onError((err, c) => {
console.error(`${err}`)
return c.json({ error: err.message }, 500)
})Rendering
Set Renderer
Define a layout once, use everywhere:
app.use(async (c, next) => {
c.setRenderer((content) => {
return c.html(
<html>
<body>
<header>My Site</header>
<main>{content}</main>
</body>
</html>
)
})
await next()
})
app.get('/', (c) => {
return c.render(<h1>Home</h1>)
})
app.get('/about', (c) => {
return c.render(<h1>About</h1>)
})With custom arguments:
declare module 'hono' {
interface ContextRenderer {
(
content: string | Promise<string>,
props: { title: string }
): Response | Promise<Response>
}
}
app.use(async (c, next) => {
c.setRenderer((content, props) => {
return c.html(
<html>
<head>
<title>{props.title}</title>
</head>
<body>{content}</body>
</html>
)
})
await next()
})
app.get('/', (c) => {
return c.render(<h1>Home</h1>, { title: 'Home Page' })
})Handler Types
Basic Handler
import type { Context } from 'hono'
const handler = (c: Context) => {
return c.text('Hello')
}
app.get('/', handler)Typed Handler
import type { Handler } from 'hono'
const handler: Handler = (c) => {
return c.text('Hello')
}Handler with Environment
import type { Handler } from 'hono'
type Env = {
Bindings: {
DB: D1Database
}
Variables: {
user: User
}
}
const handler: Handler<Env> = (c) => {
const db = c.env.DB // Typed!
const user = c.var.user // Typed!
return c.json({ user })
}Common Patterns
Async Handlers
Handlers can be async:
app.get('/users', async (c) => {
const users = await db.query('SELECT * FROM users')
return c.json({ users })
})Early Returns
Return early for validation:
app.get('/posts/:id', async (c) => {
const id = c.req.param('id')
if (!id.match(/^[0-9]+$/)) {
return c.text('Invalid ID', 400)
}
const post = await db.getPost(id)
if (!post) {
return c.notFound()
}
return c.json({ post })
})Multiple Response Types
Return different responses based on conditions:
app.get('/data', (c) => {
const format = c.req.query('format')
const data = { message: 'Hello' }
if (format === 'json') {
return c.json(data)
}
if (format === 'text') {
return c.text(data.message)
}
return c.html(`<p>${data.message}</p>`)
})What’s Next?
- Learn validation: Validation Guide
- Use middleware: Middleware Concept
- API reference: Context API