Saturday, October 18, 2025
My talk from Hono Conference 2025
I’m very excited to be speaking at Hono Conference 2025. As a permalink for what I’ll be speaking about there, this post is an overview of my talk, pertinent links, and other things related to the talk.
Resources
- My slides for the talk are available here
- You can learn more about Cloudflare Workers on the website: workers.cloudflare.com
- If you haven’t built on Workers before, consider watching my Cloudflare Workers 101 video to get started
Cloudflare Workers x Hono
My talk is about Cloudflare Workers and Hono - how they pair well together, and my history with each of them. Here’s some longer form thoughts on that.
I began working at Cloudflare as a developer advocate in 2019. At that time, Workers was pretty new. But the platform was very powerful. I came from a background of building on AWS Lambda, so I was familiar with the idea of building serverless applications - specifically, with Node. Workers was (and still is) not Node, so in addition to learning the differences between the Workers platform (not being able to use express
being one of the primary differences) there was also ergonomic differences in writing Workers applications. The standard Workers application in 2019 was implemented as a service worker, using an event listener to hook into a fetch
event and return a response:
addEventListener("fetch", event => {
return new Response("Hello, world")
})
This code was concise, but I quickly learned it didn’t scale well to full applications. Specifically, routing became a primary concern in many of the fundamental tutorials I wrote in 2019-2020 for the Cloudflare Workers documentation. There were some solutions that popped up (itty-router
was one of the primary precursors to Hono that I became familiar with), but there wasn’t a true full-stack routing system for Workers that felt native to the platform.
The second variant of a Workers application became available later, called a “module worker”. This format looked like an ES module, and was built to eventually support multiple classes and entrypoints inside of a single application. The syntax was even more concise, which was great:
export default {
fetch: request => {
return new Response("Hello, world")
}
}
As the platform matured, and the platform made use of module workers more effectively, you could define additional events inside of that module, as well as other classes that could be exported side-by-side with the module, such as Durable Object classes:
export MyDurableObject extends DurableObject {
function fetch(event) {
return new Response("Hello from a Durable Object")
}
}
export default {
fetch: request => {
return new Response("Hello, world")
},
scheduled: event => {
// Handle a reoccurring scheduled event
}
}
In the following years, the bindings concept in Cloudflare Workers became more poewrful, and ubiquitous in large-scale Workers applications. Bindings allowed the Workers runtime to hook various resources from the larger ecosystem (what we began to thematically refer to as the “Cloudflare Developer Platform”, or “Workers Platform”) directly into your Workers application. This meant that tools like Workers KV, an eventually-consistent key-value store, and later, Cloudflare D1, our SQLite database, could be used directly in Workers applications without any additional setup code - you could create the resource, define the binding in a configuration file, and it became usable immediately:
# wrangler.toml
name = "my-workers-app"
[[kv-namespaces]]
id = "f39f24ff-15c2-4dbd-a21e-b0d657bef48f"
binding = "KV"
The KV namespace, identified via the namespace ID, became available as KV
:
export default {
fetch: async (request, env) => {
const message = await env.KV.get("message")
return new Response(message || "Hello, world")
}
}
In short, there were a number of additions to the ecosystem that made Workers incredibly compelling from an ergonomics perspective. It was concise, and powerful platform-level primitives were usable via just a few lines of code. But it still was missing a fundamental way to built large-scale, fullstack applications in a friendly way.
Enter Hono
I first came across Hono in 2021 via a pull request. During that time, I spent a good part of my day-to-day reviewing pull requests, and helping grow our documentation for Workers and the rest of the associated developer platform tools. I’m sad to say that I missed the initial PR where Yusuke Wada, the creator of Hono, added it to our examples section of the Workers docs, but I caught the second PR, with a few typos and bugfixes. I don’t think we had a large amount of interest in Workers (at least, from my knowledge at the time) from developers in Japan, so seeing Yusuke contribute a pull request caught my eye. I followed the link he shared to hono.dev, to check out the framework.
I was immediately very impressed. Hono looked like a great solution to the problem we had faced, not just on the developer relations team, but on the Workers platform as a whole. A routing system for Workers, combined with first-class support for bindings.
Imagine we built a simple API system for both reading and writing to a KV namespace. Any given key could be specified via a URL pathname, with HTTP methods (GET and POST) used as the differentiator between reading or writing. In vanilla Workers, it would look something like this:
export default {
fetch: async (request, env) => {
const { method, url } = request.url
const u = new URL(url)
const key = u.pathname.replace('/', '')
if (method == "POST") {
const body = await request.json()
if (body.value) {
await env.KV.put(key, value)
return new Response("OK")
} else {
return new Response("Missing value in body", {
status: 402
})
}
} elsif (method == "GET") {
const value = await env.KV.get(key)
if (!value) {
return new Response("No message found", {
status: 502 // TODO: is this the right status code?
})
} else {
return new Response(value)
}
} else {
return new Response("Method not allowed", {
status: 405
})
}
}
}
There’s a lot of compromises here in order to make this API work cleanly. With no native routing, we match on the inbound request method, and do some sketchy string replacement to approximate a URL-driven “key”. Reading JSON out of the request body is similarly brittle. We could grade this as B- code: it certainly gets the job done, but it won’t hold up to scrutiny and is pretty easy to crash.
Moving this to Hono immediately condenses the code:
const app = new Hono()
app.get("/:key", async c => {
const { key } = c.req.params
const value = await c.env.KV.get(key)
if (!value) {
return c.text("No message found", 502)
} else {
return c.text(value)
}
})
app.post("/:key", async c => {
const body = await c.req.json()
if (body.value) {
await c.env.KV.put(key, value)
return c.text("OK")
} else {
return c.text("Missing value in body", 402)
}
})
app.all("/:key", c => {
return c.text("Method not allowed", 405)
})
(I had more to say here originally, but tbh I lost steam and I need to work on my slides. Sorry!)