Summarize with AI:
Learn about Hono routing techniques, context management, middleware patterns, server-side rendering with JSX and how to deploy an app to Cloudflare Workers.
In this post, we introduce Hono, a multi-runtime web framework that’s extremely fast and lightweight. We’ll learn about Hono’s routing techniques, context management, middleware patterns and server-side rendering with JSX, and how to deploy our app to Cloudflare Workers. We’ll build a multicurrency conversion API that runs locally on Node.js and deploys to Cloudflare Workers on the free tier.
Hono is a small, fast and simple web framework built with web standards so it can run anywhere. It’s an Express alternative designed especially for modern serverless and edge computing environments. It has built-in TypeScript support and is very compatible with Cloudflare because the creator designed it with Cloudflare in mind.
Cloudflare is a serverless edge computing environment that makes it easy to build, deploy and scale apps across a global network. Instead of using heavy virtual machines or containers, it uses Google’s V8 engine isolates, which eliminates cold start times. When an API call comes in, Cloudflare Workers intercept the request and run your code on the nearest server to you, minimizing latency.
To get started, run this command in your terminal:
npm create hono@latest currency-conver-demo
This creates a new project directory with the recommended structure and configuration files.

Next, run this command in your terminal to cd into your new directory:
cd currency-conver-demo
To start a local dev server, run this command:
npx wrangler dev
Wrangler is Cloudflare’s CLI tool that comes with your project. It helps you manage and deploy your Hono app.
Like Express, Hono has app.get(), app.post() and other HTTP methods. You can also use regular expressions to customize routes. For example, you can limit a route to specific currency codes as shown below:
// /convert/USD/NGN ✓ /convert/us/ngn ✗
app.get('/convert/:from{[A-Z]{3}}/:to{[A-Z]{3}}', (c) => {
const from = c.req.param('from')
const to = c.req.param('to')
return c.text(`Converting from ${from} to ${to}`)
})
You can also define routes to match multiple paths using the wildcard *:
// /history/2024 ✓ /history/2024/01 ✓ /history/2024/01/15 ✓
app.get('/history/*', (c) => {
return c.text('Fetching history...')
})
Or define optional parameters using ?:
// /convert/USD/NGN ✓ /convert/USD/NGN/100 ✓
app.get('/convert/:from/:to/:amount?', (c) => {
const amount = c.req.param('amount') ?? '1'
return c.text(`Converting ${amount} from USD to NGN`)
})
Finally, you can group related routes with a common prefix using app.route():
const converterRoutes = new Hono()
converterRoutes.get('/rates', (c) => c.json({ rates: RATES }))
converterRoutes.get('/convert/:from/:to', (c) => c.text('Converting...'))
app.route('/api', converterRoutes)
// now accessible at /api/rates and /api/convert/:from/:to
While Express uses req and res, Hono uses a single context object called c. This object is passed to every middleware and route handler.
app.get('/example', (c) => {
const url = c.req.url // read request data
return c.json({ url }) // send response
})
You can also use c to pass data between middleware and handlers with c.set() and c.get():
// middleware sets a value
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
// route handler reads it
app.get('/example', (c) => {
const requestId = c.get('requestId')
return c.json({ requestId })
})
We’ll use this pattern later when creating custom middleware.
Middleware functions look like route handlers, but with an extra next argument.
app.use('*', async (c, next) => {
// perform a task
await next()
})
The path determines where the middleware runs. In the example above, we used the wildcard so it runs on all routes.
Calling await next() passes control to the next middleware or route handler in the chain. The code after await next() doesn’t run until the response is built. This makes it ideal for logging, adding headers and other post-processing tasks.
The execution order looks like this:
app.use('*', async (c, next) => {
console.log('a')
await next()
console.log('-a')
})
app.use('*', async (c, next) => {
console.log('b')
await next()
console.log('-b')
})
app.use('*', async (c, next) => {
console.log('c')
await next()
console.log('-c')
})
app.get('/', (c) => c.text('Hello'))
// Output:
// a
// b
// c
// -c
// -b
// -a
Once the response is built, execution unwinds in reverse order. The last middleware to run before the route handler resumes after the handler finishes; then execution continues back through the rest of the middleware stack.
You can also define middleware separately using the factory pattern and register it wherever you need it:
import { createMiddleware } from 'hono/factory';
export const logger = createMiddleware(async (c, next) => {
await next();
console.log(`...${c.res.status}`)
})
Then register it using:
app.use('*', logger)
Hono comes with some built-in middleware. Here are two commonly used examples:
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
app.use('/api/*', cors()) // Adds CORS headers and handles preflight requests
app.use('*', prettyJSON()) // Formats JSON responses with indentation
You can also find third-party middleware here.
First, open tsconfig.json and add "WebWorker" to the lib array. Save the file before continuing. This gives TypeScript access to the types available in the Cloudflare Workers runtime:
{
"compilerOptions": {
"lib": ["ESNext", "WebWorker"]
}
}
Next, create a file called src/types.ts to define the context type that will be shared across all Hono instances. We’re doing this because any value you attach to the context with c.set() needs to be declared first:
export type Variables = {
requestId: string
}
Next, let’s create a file called src/middleware/logger.ts to define our custom logger middleware using the factory pattern:
import { createMiddleware } from 'hono/factory';
export const logger = createMiddleware(async (c, next) => {
console.log(`... ${c.req.method} ${c.req.path}`);
c.set('requestId', crypto.randomUUID());
await next();
console.log(`...${c.res.status}`)
})
Next, let’s create a file called src/views/Home.tsx to define our JSX component that we’ll render in our index route. Any file containing JSX needs the .tsx extension:
export const Home = () => {
return (
<html>
<body>
<h1>Currency Converter API</h1>
<p>Available endpoints:</p>
<ul>
<li>GET /api/rates</li>
<li>GET /api/convert/:from/:to</li>
<li>GET /api/convert/:from/:to/:amount</li>
<li>GET /api/history/*</li>
</ul>
</body>
</html>
)
}
Next, create a file called src/routes/index.tsx to set up JSX rendering. We’re going to register the jsxRenderer() middleware, which adds a render() method to our context object. This method allows us to serve the Home component as HTML:
import { Hono } from 'hono'
import { jsxRenderer } from 'hono/jsx-renderer'
import { Home } from '../views/Home'
export const indexRoutes = new Hono()
indexRoutes.use(jsxRenderer())
indexRoutes.get('/', (c) => {
return c.render(<Home />)
})
In this route, we didn’t use the context type we defined because we’re not using the requestId variable. We’ll use it in the next route.
Next, create a file called src/routes/converter.ts to combine all our routing concepts: regex constraints, wildcards, optional parameters and context access:
import { Hono } from 'hono';
import { Variables } from '../types';
const RATES: Record<string, number> = {
USD: 1,
EUR: 0.94,
GBP: 0.8,
NGN: 1400,
JPY: 149,
};
export const converterRoutes = new Hono<{ Variables: Variables }>();
converterRoutes.get('/rates', (c) => {
const requestId = c.get('requestId')
return c.json({
requestId,
rates: RATES
})
});
converterRoutes.get('/convert/:from{[A-Z]{3}}/:to{[A-Z]{3}}/:amount?', (c) => {
const from = c.req.param('from')
const to = c.req.param('to')
const amount = Number(c.req.param('amount') ?? 1)
if (!RATES[from] || !RATES[to]) {
return c.json({ error: 'Unsupported currency code' }, 400)
}
const result = (amount / RATES[from]) * RATES[to]
return c.json({ from, to, amount, result })
});
converterRoutes.get('/history/*', (c) => {
return c.json({ message: 'History feature is coming soon' })
});
Notice that because this Hono instance uses the requestId context variable, it needs to be typed with Variables:
export const converterRoutes = new Hono<{ Variables: Variables }>();
Finally, in src/index.ts, we bring everything together. We register our custom middleware, mount our route groups and export the app:
import { Hono } from 'hono';
import { Variables } from './types';
import { logger } from './middleware/logger';
import { converterRoutes } from './routes/converter';
import { indexRoutes } from './routes/index';
const app = new Hono<{ Variables: Variables }>()
app.use('*', logger)
app.route('/', indexRoutes)
app.route('/api', converterRoutes)
export default app
Note that export default app is what Cloudflare Workers expects. It’s the entry point that the runtime calls when a request comes in.
Save all files, start your app in development mode, and test the routes to make sure everything works.
npx wrangler dev
Next, run the command below and sign in to your Cloudflare account:
npx wrangler deploy
That’s it! Your app is now live on Cloudflare Workers.
You’ll see a success response like this:

In this post, we covered Hono’s routing techniques, context management, middleware patterns, server-side rendering with JSX and deploying your Hono app to Cloudflare Workers.
To further improve your app, you can use other Hono-built-in or third-party middleware, implement testing strategies and use Hono’s RPC (Remote Procedure Call) feature to share API specs between the server and client.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.