Appearance
Middleware
Middleware is code that runs between receiving a request and sending a response. Think of it like security checkpoints at an airport - every request passes through them before reaching the destination.
What is Middleware?
Middleware can:
- ✅ Run code before your route handler
- ✅ Run code after your route handler
- ✅ Modify the request or response
- ✅ Stop the request (e.g., if not authenticated)
- ✅ Pass data to the next middleware/handler
Visual Flow
Request comes in
↓
┌─────────────────┐
│ Middleware 1 │ ← Runs first (e.g., logging)
│ (before) │
└─────────────────┘
↓
┌─────────────────┐
│ Middleware 2 │ ← Runs second (e.g., auth check)
│ (before) │
└─────────────────┘
↓
┌─────────────────┐
│ Route Handler │ ← Your actual code
└─────────────────┘
↓
┌─────────────────┐
│ Middleware 2 │ ← Runs after handler
│ (after) │
└─────────────────┘
↓
┌─────────────────┐
│ Middleware 1 │ ← Runs last
│ (after) │
└─────────────────┘
↓
Response sent backBasic Middleware
Your First Middleware
typescript
import { Hono } from 'hono'
const app = new Hono()
// This middleware runs for EVERY request
app.use('*', async (c, next) => {
console.log('🚀 Request started!')
console.log(`📍 ${c.req.method} ${c.req.path}`)
await next() // Continue to next middleware/handler
console.log('✅ Request finished!')
})
app.get('/', (c) => {
console.log('👋 Handler running')
return c.text('Hello!')
})
export default appWhen you visit /, you'll see:
🚀 Request started!
📍 GET /
👋 Handler running
✅ Request finished!Understanding next()
The next() function is crucial - it passes control to the next middleware or handler:
typescript
// WITHOUT next() - request stops here!
app.use('*', async (c, next) => {
console.log('Start')
// Forgot next()!
})
// WITH next() - continues to handler
app.use('*', async (c, next) => {
console.log('Start')
await next() // ← Important!
console.log('End')
})Built-in Middleware
Hono comes with useful middleware ready to use.
Logger - See All Requests
typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()
// Add logging
app.use('*', logger())
app.get('/', (c) => c.text('Hello!'))
app.get('/users', (c) => c.json([]))
export default appOutput in console:
<-- GET /
--> GET / 200 5ms
<-- GET /users
--> GET /users 200 2msCORS - Allow Cross-Origin Requests
When your frontend (e.g., React) calls your API, you need CORS:
typescript
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
// Allow ALL origins (for development)
app.use('/api/*', cors())
// Or be specific (for production)
app.use('/api/*', cors({
origin: 'https://mywebsite.com', // Only allow this domain
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true // Allow cookies
}))
// Allow multiple origins
app.use('/api/*', cors({
origin: ['https://mywebsite.com', 'https://admin.mywebsite.com']
}))
app.get('/api/data', (c) => c.json({ data: 'value' }))
export default appBasic Auth - Simple Username/Password
typescript
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
const app = new Hono()
// Protect admin routes
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret123'
}))
// Public route - anyone can access
app.get('/', (c) => c.text('Public page'))
// Protected route - needs username/password
app.get('/admin/dashboard', (c) => {
return c.text('Welcome to admin dashboard!')
})
export default appWhat happens:
- Visit
/→ Works normally - Visit
/admin/dashboard→ Browser asks for username/password
Bearer Auth - Token Authentication
typescript
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
const app = new Hono()
// Simple token check
app.use('/api/*', bearerAuth({
token: 'my-secret-token-12345'
}))
// Client must send: Authorization: Bearer my-secret-token-12345
app.get('/api/data', (c) => {
return c.json({ secret: 'data' })
})
export default appTo access:
bash
curl http://localhost:8787/api/data \
-H "Authorization: Bearer my-secret-token-12345"Pretty JSON - Formatted JSON Output
typescript
import { Hono } from 'hono'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
app.use('*', prettyJSON())
app.get('/data', (c) => {
return c.json({
user: { name: 'John', age: 30 },
posts: [1, 2, 3]
})
})
export default appWithout ?pretty:
json
{"user":{"name":"John","age":30},"posts":[1,2,3]}With ?pretty:
json
{
"user": {
"name": "John",
"age": 30
},
"posts": [1, 2, 3]
}Secure Headers - Add Security Headers
typescript
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// Add security headers to all responses
app.use('*', secureHeaders())
app.get('/', (c) => c.text('Secure page'))
export default appAdds headers like:
X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=block
Compress - Smaller Responses
typescript
import { Hono } from 'hono'
import { compress } from 'hono/compress'
const app = new Hono()
// Compress all responses
app.use('*', compress())
app.get('/big-data', (c) => {
// This large response will be compressed
return c.json({ data: 'lots of data...'.repeat(1000) })
})
export default appCreating Custom Middleware
Simple Custom Middleware
typescript
import { Hono } from 'hono'
const app = new Hono()
// Custom middleware: Add request timing
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
console.log(`Request took ${duration}ms`)
})
app.get('/', (c) => c.text('Hello!'))
export default appRequest ID Middleware
Give every request a unique ID for tracking:
typescript
import { Hono } from 'hono'
const app = new Hono()
// Add unique ID to each request
app.use('*', async (c, next) => {
// Check if client sent an ID, or create new one
const requestId = c.req.header('X-Request-ID') || crypto.randomUUID()
// Store it for later use
c.set('requestId', requestId)
// Add to response
c.header('X-Request-ID', requestId)
await next()
})
app.get('/', (c) => {
const requestId = c.get('requestId')
return c.json({
message: 'Hello!',
requestId: requestId
})
})
export default appAuthentication Middleware
typescript
import { Hono } from 'hono'
// Define types for our variables
type Variables = {
user: {
id: string
name: string
role: string
}
}
const app = new Hono<{ Variables: Variables }>()
// Authentication middleware
const authMiddleware = async (c, next) => {
// Get token from header
const token = c.req.header('Authorization')?.replace('Bearer ', '')
// No token? Reject!
if (!token) {
return c.json({ error: 'No token provided' }, 401)
}
// Invalid token? Reject!
// (In real app, verify JWT here)
if (token !== 'valid-token') {
return c.json({ error: 'Invalid token' }, 401)
}
// Token is good! Set user info
c.set('user', {
id: '123',
name: 'John Doe',
role: 'user'
})
await next()
}
// Public routes (no auth needed)
app.get('/', (c) => c.text('Welcome!'))
app.get('/public', (c) => c.text('Public page'))
// Protected routes (auth required)
app.use('/api/*', authMiddleware)
app.get('/api/profile', (c) => {
const user = c.get('user')
return c.json({
message: `Hello ${user.name}!`,
user: user
})
})
app.get('/api/settings', (c) => {
const user = c.get('user')
return c.json({
user: user.name,
settings: { theme: 'dark' }
})
})
export default appTesting:
bash
# Without token - fails
curl http://localhost:8787/api/profile
# {"error":"No token provided"}
# With valid token - works!
curl http://localhost:8787/api/profile \
-H "Authorization: Bearer valid-token"
# {"message":"Hello John Doe!","user":{...}}Admin Only Middleware
typescript
// Check if user is admin
const adminOnly = async (c, next) => {
const user = c.get('user')
if (!user) {
return c.json({ error: 'Not authenticated' }, 401)
}
if (user.role !== 'admin') {
return c.json({ error: 'Admin access required' }, 403)
}
await next()
}
// Usage: Chain middlewares
app.get('/admin/users',
authMiddleware, // First: check if logged in
adminOnly, // Then: check if admin
(c) => {
return c.json({ users: [] })
}
)Rate Limiting Middleware
Prevent too many requests:
typescript
import { Hono } from 'hono'
const app = new Hono()
// Simple rate limiter
const rateLimiter = () => {
const requests = new Map() // Store: IP -> { count, resetTime }
return async (c, next) => {
const ip = c.req.header('x-forwarded-for') || 'unknown'
const now = Date.now()
const windowMs = 60 * 1000 // 1 minute
const maxRequests = 10 // 10 requests per minute
const record = requests.get(ip)
// Reset if window expired
if (!record || now > record.resetTime) {
requests.set(ip, {
count: 1,
resetTime: now + windowMs
})
await next()
return
}
// Too many requests?
if (record.count >= maxRequests) {
return c.json({
error: 'Too many requests',
retryAfter: Math.ceil((record.resetTime - now) / 1000)
}, 429)
}
// Increment count
record.count++
await next()
}
}
// Apply rate limiting to API
app.use('/api/*', rateLimiter())
app.get('/api/data', (c) => {
return c.json({ data: 'value' })
})
export default appError Handling Middleware
Catch all errors in one place:
typescript
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Error handler middleware
app.use('*', async (c, next) => {
try {
await next()
} catch (error) {
console.error('Error caught:', error)
// Handle known HTTP errors
if (error instanceof HTTPException) {
return c.json({
error: error.message,
status: error.status
}, error.status)
}
// Handle unknown errors
return c.json({
error: 'Internal Server Error',
message: error.message
}, 500)
}
})
// Route that might throw
app.get('/users/:id', (c) => {
const id = c.req.param('id')
if (id === '0') {
throw new HTTPException(400, { message: 'Invalid user ID' })
}
if (id === '999') {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json({ id, name: 'John' })
})
// Route with unexpected error
app.get('/crash', (c) => {
throw new Error('Something went wrong!')
})
export default appMiddleware for Specific Routes
Apply to All Routes
typescript
// '*' means all routes
app.use('*', logger())
app.use('*', cors())Apply to Path Prefix
typescript
// Only /api/* routes
app.use('/api/*', authMiddleware)
// Only /admin/* routes
app.use('/admin/*', adminOnly)Apply to Single Route
typescript
// Only this specific route
app.get('/protected',
authMiddleware, // Middleware
(c) => { // Handler
return c.text('Secret content')
}
)Multiple Middlewares on Route
typescript
app.post('/admin/users',
authMiddleware, // 1. Check login
adminOnly, // 2. Check admin
rateLimiter(), // 3. Rate limit
async (c) => { // 4. Handler
const body = await c.req.json()
return c.json({ created: body })
}
)Complete Example: API with Multiple Middleware
typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
import { prettyJSON } from 'hono/pretty-json'
// Types
type Variables = {
user: { id: string; name: string; role: 'user' | 'admin' } | null
requestId: string
}
const app = new Hono<{ Variables: Variables }>()
// ===== GLOBAL MIDDLEWARE (runs on ALL requests) =====
// 1. Add request ID
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
c.header('X-Request-ID', c.get('requestId'))
await next()
})
// 2. Timing
app.use('*', async (c, next) => {
const start = Date.now()
await next()
c.header('X-Response-Time', `${Date.now() - start}ms`)
})
// 3. Logging
app.use('*', logger())
// 4. Security headers
app.use('*', secureHeaders())
// 5. Pretty JSON
app.use('*', prettyJSON())
// ===== API MIDDLEWARE =====
// CORS for API routes
app.use('/api/*', cors({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE']
}))
// Auth for API routes
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token === 'user-token') {
c.set('user', { id: '1', name: 'John', role: 'user' })
} else if (token === 'admin-token') {
c.set('user', { id: '2', name: 'Admin', role: 'admin' })
} else {
c.set('user', null)
}
await next()
})
// ===== HELPER MIDDLEWARE =====
// Require authentication
const requireAuth = async (c, next) => {
if (!c.get('user')) {
return c.json({
error: 'Authentication required',
requestId: c.get('requestId')
}, 401)
}
await next()
}
// Require admin role
const requireAdmin = async (c, next) => {
const user = c.get('user')
if (!user || user.role !== 'admin') {
return c.json({
error: 'Admin access required',
requestId: c.get('requestId')
}, 403)
}
await next()
}
// ===== ROUTES =====
// Public
app.get('/', (c) => {
return c.json({
message: 'Welcome to the API!',
requestId: c.get('requestId')
})
})
// Public API
app.get('/api/products', (c) => {
return c.json({
products: [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 699 }
]
})
})
// Requires login
app.get('/api/profile', requireAuth, (c) => {
return c.json({
user: c.get('user')
})
})
// Requires login
app.get('/api/orders', requireAuth, (c) => {
const user = c.get('user')!
return c.json({
userId: user.id,
orders: [
{ id: 1, total: 99.99, status: 'delivered' }
]
})
})
// Requires admin
app.get('/api/admin/users', requireAuth, requireAdmin, (c) => {
return c.json({
users: [
{ id: 1, name: 'John', role: 'user' },
{ id: 2, name: 'Admin', role: 'admin' }
]
})
})
app.post('/api/admin/users', requireAuth, requireAdmin, async (c) => {
const body = await c.req.json()
return c.json({
message: 'User created',
user: { id: 999, ...body }
}, 201)
})
// ===== 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(`[${c.get('requestId')}] Error:`, err)
return c.json({
error: 'Internal Server Error',
requestId: c.get('requestId')
}, 500)
})
export default appTesting the API:
bash
# Public - works without auth
curl http://localhost:8787/
curl http://localhost:8787/api/products
# Protected - needs user token
curl http://localhost:8787/api/profile \
-H "Authorization: Bearer user-token"
# Admin - needs admin token
curl http://localhost:8787/api/admin/users \
-H "Authorization: Bearer admin-token"
# Wrong token - gets 403
curl http://localhost:8787/api/admin/users \
-H "Authorization: Bearer user-token"Summary
In this chapter, you learned:
- ✅ What middleware is and how it works
- ✅ Built-in middleware (logger, CORS, auth, etc.)
- ✅ Creating custom middleware
- ✅ Authentication and authorization
- ✅ Rate limiting
- ✅ Error handling
- ✅ Applying middleware to specific routes
- ✅ Building a complete API with multiple middleware layers
What's Next?
In the next chapter, we'll explore the Context API in detail:
- All request methods (params, query, body, headers)
- All response helpers (json, text, html, redirect)
- Context variables and type safety
- Environment bindings