Appearance
Context API
The Context object (c) is your best friend in Hono. It gives you everything you need to handle requests and send responses. Let's explore it in detail!
What is Context?
Every route handler receives a Context object. Think of it as a toolbox:
typescript
app.get('/example', (c) => {
// 'c' is the Context object
// It has tools for:
// - Reading the request (c.req)
// - Sending responses (c.json, c.text, etc.)
// - Storing data (c.set, c.get)
// - Accessing environment (c.env)
return c.text('Hello!')
})Reading Request Data
Get URL Parameters
URL parameters are the :something parts in your routes:
typescript
// Route: /users/:id
// URL: /users/123
app.get('/users/:id', (c) => {
// Get single parameter
const id = c.req.param('id') // '123'
return c.json({ userId: id })
})
// Multiple parameters
// Route: /users/:userId/posts/:postId
// URL: /users/5/posts/10
app.get('/users/:userId/posts/:postId', (c) => {
// Get individually
const userId = c.req.param('userId') // '5'
const postId = c.req.param('postId') // '10'
// Or get all at once
const allParams = c.req.param()
// { userId: '5', postId: '10' }
return c.json({ userId, postId })
})Get Query Parameters
Query parameters come after ? in the URL:
typescript
// URL: /search?q=hono&page=2&limit=10
app.get('/search', (c) => {
// Get single query param
const query = c.req.query('q') // 'hono'
const page = c.req.query('page') // '2'
const limit = c.req.query('limit') // '10'
// Missing param returns undefined
const missing = c.req.query('foo') // undefined
// Get all query params
const allQueries = c.req.query()
// { q: 'hono', page: '2', limit: '10' }
return c.json({
searchQuery: query,
page: parseInt(page || '1'),
limit: parseInt(limit || '10')
})
})
// Multiple values with same key
// URL: /filter?tag=js&tag=ts&tag=hono
app.get('/filter', (c) => {
// queries() returns an array
const tags = c.req.queries('tag')
// ['js', 'ts', 'hono']
return c.json({ tags })
})Get Request Headers
typescript
app.get('/check-headers', (c) => {
// Common headers
const contentType = c.req.header('Content-Type')
const auth = c.req.header('Authorization')
const userAgent = c.req.header('User-Agent')
const accept = c.req.header('Accept')
// Custom headers
const apiKey = c.req.header('X-API-Key')
const requestId = c.req.header('X-Request-ID')
// Headers are case-insensitive
const same1 = c.req.header('content-type')
const same2 = c.req.header('Content-Type')
// Both return the same value
return c.json({
contentType,
hasAuth: !!auth,
userAgent,
apiKey
})
})Get Request Body
JSON Body (Most Common)
typescript
app.post('/users', async (c) => {
// Parse JSON body
const body = await c.req.json()
// Example body: { "name": "John", "email": "john@example.com" }
console.log(body.name) // 'John'
console.log(body.email) // 'john@example.com'
return c.json({
message: 'User created',
user: body
}, 201)
})
// With TypeScript types
interface CreateUserBody {
name: string
email: string
age?: number
}
app.post('/users', async (c) => {
const body = await c.req.json<CreateUserBody>()
// Now TypeScript knows the shape of body!
return c.json({ name: body.name, email: body.email })
})Form Data
typescript
// HTML form: <form method="POST" action="/login">
// <input name="email" />
// <input name="password" type="password" />
// </form>
app.post('/login', async (c) => {
const body = await c.req.parseBody()
const email = body.email // 'user@example.com'
const password = body.password // 'secret123'
return c.json({ email })
})File Upload
typescript
// HTML form: <form method="POST" enctype="multipart/form-data">
// <input name="file" type="file" />
// <input name="description" />
// </form>
app.post('/upload', async (c) => {
const body = await c.req.parseBody()
// Get the file
const file = body.file as File
// Get other form fields
const description = body.description as string
// File properties
console.log(file.name) // 'photo.jpg'
console.log(file.size) // 12345 (bytes)
console.log(file.type) // 'image/jpeg'
// Read file content
const content = await file.arrayBuffer()
return c.json({
filename: file.name,
size: file.size,
type: file.type,
description
})
})Raw Text Body
typescript
app.post('/webhook', async (c) => {
// Get raw text
const rawBody = await c.req.text()
console.log(rawBody) // The raw string
return c.text('Received')
})Get Other Request Info
typescript
app.get('/info', (c) => {
// HTTP method
const method = c.req.method // 'GET', 'POST', etc.
// Full URL
const url = c.req.url
// 'http://localhost:8787/info?foo=bar'
// Just the path
const path = c.req.path // '/info'
// The raw Request object (Web standard)
const rawRequest = c.req.raw
return c.json({
method,
url,
path
})
})Sending Responses
Text Response
typescript
// Simple text
app.get('/hello', (c) => {
return c.text('Hello World!')
})
// With status code
app.get('/not-found', (c) => {
return c.text('Page not found', 404)
})
// With headers
app.get('/custom', (c) => {
return c.text('Hello', 200, {
'X-Custom-Header': 'value',
'Cache-Control': 'no-cache'
})
})JSON Response (Most Common for APIs)
typescript
// Simple JSON
app.get('/user', (c) => {
return c.json({
id: 1,
name: 'John',
email: 'john@example.com'
})
})
// With status code
app.post('/users', (c) => {
return c.json(
{ id: 1, name: 'John' },
201 // Created
)
})
// Error responses
app.get('/error', (c) => {
return c.json(
{ error: 'Something went wrong' },
500 // Internal Server Error
)
})
// With custom headers
app.get('/data', (c) => {
return c.json(
{ data: 'value' },
200,
{ 'Cache-Control': 'max-age=3600' }
)
})HTML Response
typescript
// Simple HTML
app.get('/page', (c) => {
return c.html('<h1>Hello World!</h1>')
})
// Full HTML page
app.get('/home', (c) => {
return c.html(`
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<style>
body { font-family: Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome!</h1>
<p>This is my Hono app.</p>
</body>
</html>
`)
})
// Dynamic HTML
app.get('/greet/:name', (c) => {
const name = c.req.param('name')
return c.html(`
<h1>Hello, ${name}!</h1>
<p>Welcome to our site.</p>
`)
})Redirect Response
typescript
// Temporary redirect (302) - default
app.get('/old-page', (c) => {
return c.redirect('/new-page')
})
// Permanent redirect (301)
app.get('/legacy-url', (c) => {
return c.redirect('/modern-url', 301)
})
// Redirect to external URL
app.get('/go-to-google', (c) => {
return c.redirect('https://google.com')
})
// Redirect after action
app.post('/login', async (c) => {
// ... login logic ...
// Redirect to dashboard after successful login
return c.redirect('/dashboard')
})No Content Response
typescript
// 204 No Content - success but no body
app.delete('/users/:id', (c) => {
// ... delete user logic ...
return c.body(null, 204)
})Setting Response Headers
typescript
app.get('/with-headers', (c) => {
// Set individual headers
c.header('X-Custom-Header', 'my-value')
c.header('X-Another-Header', 'another-value')
// Set cache headers
c.header('Cache-Control', 'public, max-age=3600')
// Set multiple cookies
c.header('Set-Cookie', 'session=abc123', { append: true })
c.header('Set-Cookie', 'user=john', { append: true })
return c.json({ message: 'Check the headers!' })
})Setting Status Code
typescript
app.get('/status-example', (c) => {
// Set status before response
c.status(201)
return c.json({ created: true })
})Context Variables
Store data during a request (useful with middleware):
Setting Variables
typescript
// Define variable types (TypeScript)
type Variables = {
user: { id: string; name: string }
requestId: string
startTime: number
}
const app = new Hono<{ Variables: Variables }>()
// Set variables in middleware
app.use('*', async (c, next) => {
// Set values
c.set('requestId', crypto.randomUUID())
c.set('startTime', Date.now())
await next()
})
app.use('/api/*', async (c, next) => {
// Simulate getting user from token
c.set('user', { id: '123', name: 'John' })
await next()
})Getting Variables
typescript
app.get('/api/profile', (c) => {
// Get values set by middleware
const user = c.get('user')
const requestId = c.get('requestId')
const startTime = c.get('startTime')
return c.json({
user,
requestId,
processingTime: Date.now() - startTime
})
})Practical Example
typescript
type Variables = {
user: { id: string; name: string; role: string } | null
requestId: string
}
const app = new Hono<{ Variables: Variables }>()
// Add request ID to every request
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
// Try to authenticate user
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (token === 'Bearer valid-token') {
c.set('user', {
id: '1',
name: 'John',
role: 'admin'
})
} else {
c.set('user', null)
}
await next()
})
// Use the variables
app.get('/api/me', (c) => {
const user = c.get('user')
const requestId = c.get('requestId')
if (!user) {
return c.json({
error: 'Not authenticated',
requestId
}, 401)
}
return c.json({
user,
requestId
})
})Environment Variables (Bindings)
For Cloudflare Workers, access environment variables and bindings:
Basic Environment Variables
typescript
// Define binding types
type Bindings = {
API_KEY: string
DATABASE_URL: string
SECRET: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/config', (c) => {
// Access environment variables
const apiKey = c.env.API_KEY
const dbUrl = c.env.DATABASE_URL
return c.json({
hasApiKey: !!apiKey,
dbConfigured: !!dbUrl
})
})Cloudflare KV Storage
typescript
type Bindings = {
MY_KV: KVNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
// Read from KV
app.get('/kv/:key', async (c) => {
const key = c.req.param('key')
const value = await c.env.MY_KV.get(key)
if (!value) {
return c.json({ error: 'Key not found' }, 404)
}
return c.json({ key, value })
})
// Write to KV
app.post('/kv/:key', async (c) => {
const key = c.req.param('key')
const { value } = await c.req.json()
await c.env.MY_KV.put(key, value)
return c.json({ message: 'Saved', key, value })
})
// Delete from KV
app.delete('/kv/:key', async (c) => {
const key = c.req.param('key')
await c.env.MY_KV.delete(key)
return c.json({ message: 'Deleted', key })
})Cloudflare D1 Database
typescript
type Bindings = {
DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
// Query database
app.get('/users', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT * FROM users')
.all()
return c.json({ users: results })
})
// Insert into database
app.post('/users', async (c) => {
const { name, email } = await c.req.json()
const result = await c.env.DB
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(name, email)
.run()
return c.json({
message: 'User created',
id: result.lastRowId
}, 201)
})Error Handling
Using HTTPException
typescript
import { HTTPException } from 'hono/http-exception'
app.get('/users/:id', (c) => {
const id = c.req.param('id')
// Validate ID
if (isNaN(parseInt(id))) {
throw new HTTPException(400, {
message: 'Invalid user ID'
})
}
// Simulate user not found
if (id === '999') {
throw new HTTPException(404, {
message: 'User not found'
})
}
return c.json({ id, name: 'John' })
})
// Handle errors globally
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({
error: err.message,
status: err.status
}, err.status)
}
// Unknown error
return c.json({
error: 'Internal Server Error'
}, 500)
})404 Handler
typescript
app.notFound((c) => {
return c.json({
error: 'Not Found',
message: `Route ${c.req.method} ${c.req.path} not found`
}, 404)
})Complete Example
typescript
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
// Types
type Bindings = {
API_KEY: string
}
type Variables = {
user: { id: string; name: string } | null
requestId: string
}
// Create app with types
const app = new Hono<{
Bindings: Bindings
Variables: Variables
}>()
// ===== MIDDLEWARE =====
// Request ID
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
c.header('X-Request-ID', c.get('requestId'))
await next()
})
// Auth
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token === 'valid') {
c.set('user', { id: '1', name: 'John' })
} else {
c.set('user', null)
}
await next()
})
// ===== ROUTES =====
// Show request info
app.get('/', (c) => {
return c.json({
message: 'Welcome!',
requestId: c.get('requestId'),
info: {
method: c.req.method,
path: c.req.path,
url: c.req.url
}
})
})
// Query parameters example
app.get('/search', (c) => {
const query = c.req.query('q') || ''
const page = parseInt(c.req.query('page') || '1')
const limit = parseInt(c.req.query('limit') || '10')
return c.json({
query,
page,
limit,
results: []
})
})
// URL parameters example
app.get('/users/:id', (c) => {
const id = c.req.param('id')
if (isNaN(parseInt(id))) {
throw new HTTPException(400, { message: 'Invalid ID' })
}
return c.json({
id: parseInt(id),
name: 'User ' + id
})
})
// POST with JSON body
app.post('/api/posts', async (c) => {
const user = c.get('user')
if (!user) {
throw new HTTPException(401, { message: 'Login required' })
}
const body = await c.req.json<{
title: string
content: string
}>()
if (!body.title || !body.content) {
throw new HTTPException(400, {
message: 'Title and content required'
})
}
return c.json({
id: 1,
title: body.title,
content: body.content,
author: user.name,
createdAt: new Date().toISOString()
}, 201)
})
// Headers example
app.get('/headers', (c) => {
return c.json({
userAgent: c.req.header('User-Agent'),
accept: c.req.header('Accept'),
contentType: c.req.header('Content-Type')
})
})
// Different response types
app.get('/html', (c) => {
return c.html('<h1>Hello HTML!</h1>')
})
app.get('/redirect', (c) => {
return c.redirect('/')
})
// ===== ERROR HANDLING =====
app.notFound((c) => {
return c.json({
error: 'Not Found',
path: c.req.path,
requestId: c.get('requestId')
}, 404)
})
app.onError((err, c) => {
console.error('Error:', err)
if (err instanceof HTTPException) {
return c.json({
error: err.message,
requestId: c.get('requestId')
}, err.status)
}
return c.json({
error: 'Internal Server Error',
requestId: c.get('requestId')
}, 500)
})
export default appSummary
In this chapter, you learned:
- ✅ Getting URL parameters (
c.req.param()) - ✅ Getting query parameters (
c.req.query()) - ✅ Getting headers (
c.req.header()) - ✅ Getting request body (JSON, form, file)
- ✅ Sending responses (text, JSON, HTML, redirect)
- ✅ Setting response headers
- ✅ Using context variables (
c.set(),c.get()) - ✅ Accessing environment variables (
c.env) - ✅ Error handling with HTTPException
What's Next?
In the next chapter, we'll learn about Validation:
- Built-in validators
- Zod integration
- Custom validation
- Type-safe validation