Skip to Content
key-conceptsContext And Handlers

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 object
  • c.res - The response object (Web Standard Response)
  • c.env - Environment variables and bindings
  • c.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?