Website Backend Development with Node.js (Hono)
Hono is one of the few frameworks designed to work across multiple runtimes simultaneously: Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda Edge. One codebase runs on multiple platforms without adapters. This is the primary reason to choose it when your project might live on edge or needs platform flexibility.
The second argument is performance. Hono uses RegExpRouter — one of the fastest routing algorithms: O(1) for static routes, O(n) for parametric ones. On Bun it outperforms Fastify in benchmarks.
Basic Application Structure
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// Built-in middleware
app.use('*', logger())
app.use('*', secureHeaders())
app.use('/api/*', cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
credentials: true
}))
// Error handling
app.onError((err, c) => {
console.error(err)
return c.json({ error: err.message }, 500)
})
app.notFound((c) => c.json({ error: 'Not found' }, 404))
export default app
Routing and Grouping
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const products = new Hono()
const createProductSchema = z.object({
name: z.string().min(2).max(255),
price: z.number().positive(),
categoryId: z.number().int()
})
products.get('/', async (c) => {
const { page = '1', limit = '20' } = c.req.query()
const db = c.get('db')
const items = await db.query(
'SELECT * FROM products LIMIT $1 OFFSET $2',
[Number(limit), (Number(page) - 1) * Number(limit)]
)
return c.json(items.rows)
})
products.post('/',
zValidator('json', createProductSchema),
async (c) => {
const body = c.req.valid('json') // fully typed
const db = c.get('db')
// ...
return c.json({ id: newProduct.id }, 201)
}
)
products.get('/:id', async (c) => {
const id = c.req.param('id')
// ...
})
// Mounting with prefix
const api = new Hono().basePath('/api/v1')
api.route('/products', products)
api.route('/users', users)
app.route('', api)
JWT Authentication
import { jwt } from 'hono/jwt'
import { getCookie, setCookie } from 'hono/cookie'
const authMiddleware = jwt({
secret: process.env.JWT_SECRET!,
cookie: 'access_token' // supports JWT from cookies
})
// Protected routes
const protected = new Hono()
protected.use('*', authMiddleware)
protected.get('/profile', (c) => {
const payload = c.get('jwtPayload') // typed payload
return c.json({ userId: payload.sub })
})
// Login endpoint
app.post('/auth/login', zValidator('json', loginSchema), async (c) => {
const { email, password } = c.req.valid('json')
const user = await UserService.verifyCredentials(email, password)
if (!user) return c.json({ error: 'Invalid credentials' }, 401)
const token = await sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET!)
setCookie(c, 'access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 60 * 60 * 24 * 7
})
return c.json({ user: { id: user.id, email: user.email } })
})
Middleware and Context
Hono uses c.set() / c.get() to pass data through middleware — similar to ctx.state in Koa:
// Database connection via middleware
app.use('*', async (c, next) => {
c.set('db', pgPool)
await next()
})
// Type context variables
type Env = {
Variables: {
db: Pool
user: { id: number; role: string }
}
}
const app = new Hono<Env>()
Edge Deployment on Cloudflare Workers
One of Hono's key advantages is deployment to Cloudflare Workers without code changes:
// wrangler.toml
// name = "my-api"
// compatibility_date = "2024-01-01"
// src/index.ts — same code
export default app // Hono compatible with Workers
// Access Workers KV, D1, R2 via bindings
app.get('/cache/:key', async (c) => {
const kv = c.env.MY_KV // Cloudflare KV namespace
const value = await kv.get(c.req.param('key'))
return value ? c.text(value) : c.notFound()
})
This is useful for APIs that need to work with minimal latency globally — CDN edge instead of a single data center.
RPC Client
Hono supports a typed client — the frontend gets types from server code without a separate schema:
// server/routes/users.ts
const users = new Hono()
.get('/', ..., (c) => c.json({ users: [] }))
.post('/', ..., (c) => c.json({ id: 1 }, 201))
export type UsersRoutes = typeof users
// client/api.ts (Next.js, React, etc.)
import { hc } from 'hono/client'
import type { UsersRoutes } from '../server/routes/users'
const client = hc<UsersRoutes>('http://localhost:3000')
const res = await client.users.$get()
const data = await res.json() // fully typed response
When to Use Hono
Hono is well-suited for: APIs with potential edge deployment, TypeScript-first projects, BFF (Backend For Frontend) layer alongside React/Next.js, microservices with minimal footprint. Less suitable for: complex monoliths with rich ORM, projects with established Express/Koa expertise in the team.
Development Timelines
- Project setup + basic routes — 2–3 days
- Auth + validation — 3–5 days
- Business logic — 1–3 weeks depending on scope
- Edge deployment and Cloudflare configuration — 2–4 days additional
Hono is a fast start with good typing. For a small website API the complete cycle: 3–6 weeks.







