Hono on Cloudflare Workers
Overview
Hono was originally built for Cloudflare Workers and provides first-class support for the entire Cloudflare ecosystem including KV, D1, R2, Durable Objects, Queues, and more.
Key Features:
-
Native Workers support
-
Type-safe bindings access
-
KV, D1, R2, Durable Objects integration
-
Static asset serving
-
Cloudflare Pages support
-
Queue and scheduled handlers
When to Use This Skill
Use Hono on Cloudflare when:
-
Building edge APIs with global distribution
-
Need serverless SQLite with D1
-
Building real-time apps with Durable Objects
-
Storing files with R2
-
Need fast key-value storage with KV
-
Deploying full-stack apps to Pages
Quick Start
Create New Project
npm create hono@latest my-app
Select: cloudflare-workers
cd my-app npm install npm run dev
Project Structure
my-app/ ├── src/ │ └── index.ts # Main entry point ├── wrangler.toml # Cloudflare configuration ├── package.json └── tsconfig.json
Basic Application
// src/index.ts import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Cloudflare Workers!'))
export default app
Deploy
Deploy to Cloudflare
npx wrangler deploy
Local development
npx wrangler dev
Environment Bindings
Typed Bindings
import { Hono } from 'hono'
// Define your bindings type Bindings = { // Environment variables API_KEY: string DATABASE_URL: string
// KV Namespaces MY_KV: KVNamespace
// D1 Databases DB: D1Database
// R2 Buckets BUCKET: R2Bucket
// Durable Objects COUNTER: DurableObjectNamespace
// Queues MY_QUEUE: Queue }
const app = new Hono<{ Bindings: Bindings }>()
app.get('/config', (c) => { // Fully typed access const apiKey = c.env.API_KEY return c.json({ configured: !!apiKey }) })
export default app
wrangler.toml Configuration
name = "my-app" main = "src/index.ts" compatibility_date = "2024-01-01"
[vars] API_KEY = "your-api-key" # pragma: allowlist secret
[[kv_namespaces]] binding = "MY_KV" id = "your-kv-id"
[[d1_databases]] binding = "DB" database_name = "my-database" database_id = "your-d1-id"
[[r2_buckets]] binding = "BUCKET" bucket_name = "my-bucket"
[[queues.producers]] binding = "MY_QUEUE" queue = "my-queue"
KV Storage
Basic Operations
type Bindings = { CACHE: KVNamespace }
const app = new Hono<{ Bindings: Bindings }>()
// Get value app.get('/cache/:key', async (c) => { const key = c.req.param('key') const value = await c.env.CACHE.get(key)
if (!value) { return c.json({ error: 'Not found' }, 404) }
return c.json({ key, value }) })
// Get JSON value app.get('/cache/:key/json', async (c) => { const key = c.req.param('key') const value = await c.env.CACHE.get(key, 'json')
return c.json({ key, value }) })
// Set value app.put('/cache/:key', async (c) => { const key = c.req.param('key') const body = await c.req.json()
await c.env.CACHE.put(key, JSON.stringify(body), { expirationTtl: 3600 // 1 hour })
return c.json({ success: true }) })
// Delete value app.delete('/cache/:key', async (c) => { const key = c.req.param('key') await c.env.CACHE.delete(key)
return c.json({ success: true }) })
// List keys app.get('/cache', async (c) => { const prefix = c.req.query('prefix') || '' const list = await c.env.CACHE.list({ prefix, limit: 100 })
return c.json({ keys: list.keys }) })
KV with Metadata
interface UserMeta { createdAt: string role: string }
app.put('/users/:id', async (c) => { const id = c.req.param('id') const user = await c.req.json()
await c.env.CACHE.put(user:${id}, JSON.stringify(user), {
metadata: {
createdAt: new Date().toISOString(),
role: user.role
} as UserMeta
})
return c.json({ success: true }) })
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const { value, metadata } = await c.env.CACHE.getWithMetadata<UserMeta>(user:${id}, 'json')
if (!value) { return c.json({ error: 'Not found' }, 404) }
return c.json({ user: value, metadata }) })
D1 Database
Basic Queries
type Bindings = { DB: D1Database }
const app = new Hono<{ Bindings: Bindings }>()
// Select all app.get('/users', async (c) => { const { results } = await c.env.DB .prepare('SELECT * FROM users ORDER BY created_at DESC') .all()
return c.json({ users: results }) })
// Select one app.get('/users/:id', async (c) => { const id = c.req.param('id') const user = await c.env.DB .prepare('SELECT * FROM users WHERE id = ?') .bind(id) .first()
if (!user) { return c.json({ error: 'Not found' }, 404) }
return c.json({ user }) })
// Insert 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({ success: result.success, id: result.meta.last_row_id }, 201) })
// Update app.put('/users/:id', async (c) => { const id = c.req.param('id') const { name, email } = await c.req.json()
const result = await c.env.DB .prepare('UPDATE users SET name = ?, email = ? WHERE id = ?') .bind(name, email, id) .run()
return c.json({ success: result.success }) })
// Delete app.delete('/users/:id', async (c) => { const id = c.req.param('id')
const result = await c.env.DB .prepare('DELETE FROM users WHERE id = ?') .bind(id) .run()
return c.json({ success: result.success }) })
Batch Operations
app.post('/users/batch', async (c) => { const { users } = await c.req.json()
const statements = users.map((user: { name: string; email: string }) => c.env.DB .prepare('INSERT INTO users (name, email) VALUES (?, ?)') .bind(user.name, user.email) )
const results = await c.env.DB.batch(statements)
return c.json({ success: results.every(r => r.success), count: results.length }) })
R2 Object Storage
type Bindings = { BUCKET: R2Bucket }
const app = new Hono<{ Bindings: Bindings }>()
// Upload file app.post('/files/:key', async (c) => { const key = c.req.param('key') const body = await c.req.arrayBuffer() const contentType = c.req.header('Content-Type') || 'application/octet-stream'
await c.env.BUCKET.put(key, body, { httpMetadata: { contentType } })
return c.json({ success: true, key }) })
// Download file app.get('/files/:key', async (c) => { const key = c.req.param('key') const object = await c.env.BUCKET.get(key)
if (!object) { return c.json({ error: 'Not found' }, 404) }
const headers = new Headers() headers.set('Content-Type', object.httpMetadata?.contentType || 'application/octet-stream') headers.set('ETag', object.httpEtag)
return new Response(object.body, { headers }) })
// Delete file app.delete('/files/:key', async (c) => { const key = c.req.param('key') await c.env.BUCKET.delete(key)
return c.json({ success: true }) })
// List files app.get('/files', async (c) => { const prefix = c.req.query('prefix') || '' const list = await c.env.BUCKET.list({ prefix, limit: 100 })
return c.json({ objects: list.objects.map(obj => ({ key: obj.key, size: obj.size, uploaded: obj.uploaded })) }) })
Durable Objects
Define Durable Object
// src/counter.ts export class Counter { private state: DurableObjectState private value: number = 0
constructor(state: DurableObjectState) { this.state = state }
async fetch(request: Request): Promise<Response> { const url = new URL(request.url)
// Load value from storage
this.value = await this.state.storage.get('value') || 0
switch (url.pathname) {
case '/increment':
this.value++
await this.state.storage.put('value', this.value)
return new Response(String(this.value))
case '/decrement':
this.value--
await this.state.storage.put('value', this.value)
return new Response(String(this.value))
case '/value':
return new Response(String(this.value))
default:
return new Response('Not found', { status: 404 })
}
} }
Use in Hono
import { Hono } from 'hono'
type Bindings = { COUNTER: DurableObjectNamespace }
const app = new Hono<{ Bindings: Bindings }>()
app.get('/counter/:name/increment', async (c) => { const name = c.req.param('name') const id = c.env.COUNTER.idFromName(name) const stub = c.env.COUNTER.get(id)
const response = await stub.fetch('http://counter/increment') const value = await response.text()
return c.json({ name, value: parseInt(value) }) })
app.get('/counter/:name', async (c) => { const name = c.req.param('name') const id = c.env.COUNTER.idFromName(name) const stub = c.env.COUNTER.get(id)
const response = await stub.fetch('http://counter/value') const value = await response.text()
return c.json({ name, value: parseInt(value) }) })
export default app export { Counter }
wrangler.toml for Durable Objects
[[durable_objects.bindings]] name = "COUNTER" class_name = "Counter"
[[migrations]] tag = "v1" new_classes = ["Counter"]
Queues
Producer
type Bindings = { MY_QUEUE: Queue }
const app = new Hono<{ Bindings: Bindings }>()
app.post('/tasks', async (c) => { const task = await c.req.json()
await c.env.MY_QUEUE.send({ type: 'process', data: task })
return c.json({ queued: true }) })
// Batch send app.post('/tasks/batch', async (c) => { const { tasks } = await c.req.json()
await c.env.MY_QUEUE.sendBatch( tasks.map((task: any) => ({ body: { type: 'process', data: task } })) )
return c.json({ queued: tasks.length }) })
Consumer
export default { fetch: app.fetch,
async queue(batch: MessageBatch, env: Bindings): Promise<void> { for (const message of batch.messages) { const { type, data } = message.body as { type: string; data: any }
try {
// Process message
console.log(`Processing ${type}:`, data)
message.ack()
} catch (error) {
message.retry()
}
}
} }
Static Assets
wrangler.toml
name = "my-app" main = "src/index.ts" compatibility_date = "2024-01-01"
Serve static files from public directory
assets = { directory = "public" }
With Route Handler
import { Hono } from 'hono' import { serveStatic } from 'hono/cloudflare-workers'
const app = new Hono()
// Serve static files app.use('/static/*', serveStatic({ root: './' }))
// API routes app.get('/api/hello', (c) => c.json({ hello: 'world' }))
export default app
Scheduled Events (Cron)
import { Hono } from 'hono'
const app = new Hono()
// Regular routes...
export default { fetch: app.fetch,
async scheduled( event: ScheduledEvent, env: Bindings, ctx: ExecutionContext ): Promise<void> { switch (event.cron) { case '0 * * * *': // Every hour await hourlyTask(env) break
case '0 0 * * *': // Daily at midnight
await dailyTask(env)
break
}
} }
async function hourlyTask(env: Bindings) { console.log('Running hourly task') }
async function dailyTask(env: Bindings) { console.log('Running daily task') }
wrangler.toml for Cron
[triggers] crons = ["0 * * * *", "0 0 * * *"]
Cloudflare Pages
pages/functions Directory
my-app/ ├── public/ # Static assets │ ├── index.html │ └── styles.css └── functions/ └── [[path]].ts # Catch-all function
Catch-All Handler
// functions/[[path]].ts import { Hono } from 'hono' import { handle } from 'hono/cloudflare-pages'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => c.json({ hello: 'world' })) app.post('/echo', async (c) => c.json(await c.req.json()))
export const onRequest = handle(app)
Middleware with Bindings
import { createMiddleware } from 'hono/factory'
type Bindings = { API_KEY: string }
// Access env in middleware const authMiddleware = createMiddleware<{ Bindings: Bindings }>( async (c, next) => { // Don't access env at module level - access in handler! const apiKey = c.req.header('X-API-Key')
if (apiKey !== c.env.API_KEY) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
} )
app.use('/api/*', authMiddleware)
Quick Reference
Bindings Types
type Bindings = { // Variables MY_VAR: string
// KV MY_KV: KVNamespace
// D1 DB: D1Database
// R2 BUCKET: R2Bucket
// Durable Objects MY_DO: DurableObjectNamespace
// Queues MY_QUEUE: Queue
// Service Bindings OTHER_WORKER: Fetcher }
Common Commands
Local development
npx wrangler dev
Deploy
npx wrangler deploy
Create D1 database
npx wrangler d1 create my-database
Execute D1 SQL
npx wrangler d1 execute my-database --file=schema.sql
Create KV namespace
npx wrangler kv:namespace create MY_KV
Create R2 bucket
npx wrangler r2 bucket create my-bucket
Tail logs
npx wrangler tail
Related Skills
-
hono-core - Framework fundamentals
-
hono-middleware - Middleware patterns
-
hono-testing - Testing with mocked bindings
Version: Hono 4.x, Wrangler 3.x Last Updated: January 2025 License: MIT