구독해서 새 게시물에 대한 알림을 받으세요.

The story of web framework Hono, from the creator of Hono

2024-10-17

10분 읽기
이 게시물은 English로도 이용할 수 있습니다.

Hono is a fast, lightweight web framework that runs anywhere JavaScript does, built with Web Standards. Of course, it runs on Cloudflare Workers.

It was three years ago, in December 2021. At that time, I wanted to create applications for Cloudflare Workers, but the code became verbose without using a framework, and couldn't find a framework that suited my needs. Itty-router was very nice but too simple. Worktop and Sunder did the same things I wanted to do, but their APIs weren't quite to my liking. I was also interested in creating a router — a program that determines which action is executed based on the HTTP method and URL path of the Request — made of a Trie tree structure because it’s fast. So, I started building a web framework with a Trie tree-based router.

 “While trying to create my applications, I ended up creating my framework for them.” — a classic example of yak shaving. However, Hono is now used by many developers, including Cloudflare, which uses Hono in core products. So, this journey into the depths of yak shaving was ultimately meaningful.

Write once, run anywhere

Hono truly runs anywhere — not just on Cloudflare Workers. I’ll discuss why later in the post, but Hono also runs on Deno, Bun, and Node.js. This is because Hono does not depend on external libraries, but uses only the Web Standards API, and each runtime supports Web Standards.

It's a delight for developers to know that the same code can run across different runtimes. For instance, the following src/index.ts code will run on Cloudflare Workers, Deno, and Bun.

import { Hono } from 'hono'

const app = new Hono()
app.get('/hello', (c) => c.text('Hello Hono!'))

export default app

To run it on Cloudflare Workers, you execute the Wrangler command:

wrangler dev src/index.ts

The same code works on Deno:

deno serve src/index.ts

And it works on Bun too:

bun run src/index.ts

This is only a simple "Hello World" example, but more complex applications with middleware and helpers that are discussed below can be run on Cloudflare Workers or the other runtimes. As proof of this, almost all our test code for Hono itself can run the same way on these runtimes. This is a genuine "write once, run anywhere" experience.

Who is using Hono?

Hono is now used by many developers and companies. For example, Unkey deploys their application built with Hono's OpenAPI feature to Cloudflare Workers. The following is a list of companies using Hono, based on my survey "Who is using Hono in production?”.

There are many, many more companies not listed here. And major web services or libraries, such as Prisma, Resend, Vercel AI SDK, Supabase, and Upstash, use Hono in their examples. There are also several influencers who like Hono and use it as an alternative to Express.

Of course, at Cloudflare, we also use Hono. D1 uses Hono for the internal Web API running on Workers. Workers Logs is based on code from Baselime (acquired by Cloudflare) and uses Hono to migrate the applications from their original infrastructure to Cloudflare Workers. All Workers Logs internal or customer-facing APIs are run on Workers using Hono. We also use Hono as part of the internals of many other products, such as KV and Queues.

Why are you making a “multi-runtime” framework?

You might wonder “Why is an employee of Cloudflare creating a framework that runs everywhere?” Initially, Hono was designed to work exclusively with Cloudflare Workers. However, starting with version 2, I added support for Deno and Bun. This was a very wise decision. If Hono had been targeted only at Cloudflare Workers, it might not have attracted as many users. By running on more runtimes, it gains more users, leading to the discovery of bugs and receiving more feedback, which ultimately leads to higher quality software.

Hono and Cloudflare are a perfect combo

The combination of Hono and Cloudflare offers a delightful developer experience.

Many websites, including our Cloudflare Docs, introduce the following "vanilla" JavaScript as a "Hello World" for Cloudflare Workers:

export default {
  fetch: () => {
    return new Response('Hello World!')
  }
}

This is primitive and good for understanding the Workers principle. However, if you want to create an endpoint that "returns a JSON response for GET requests that come to /books", you need to write something like this:

export default {
  fetch: (req) => {
    const url = new URL(req.url)
    if (req.method === 'GET' && url.pathname === '/books') {
      return Response.json({
        ok: true
      })
    }
    return Response.json(
      {
        ok: false
      },
      {
        status: 404
      }
    )
  }
}

If you use Hono, you can write it like the following:

import { Hono } from 'hono'

const app = new Hono()

app.get('/books', (c) => {
  return c.json({
    ok: true
  })
})

export default app

It is short. And you can understand that “it handles GET accesses to /books” intuitively.

If you want to handle GET requests to /authors/yusuke and get "yusuke" from the path —  "yusuke" is variable, you have to add something more complicated. The below is "vanilla" JavaScript example:

if (req.method === 'GET') {
  const match = url.pathname.match(/^\/authors\/([^\/]+)/)
  if (match) {
    const author = match[1]
    return Response.json({
      Author: author
    })
  }
}

If you use Hono, you don't need if statements. Just add the endpoint definition to the app. Also, you don't need to write a regular expression to get "yusuke". You can get it with the function c.req.param():

app.get('/authors/:name', (c) => {
  const author = c.req.param('name')
  return c.json({
    Author: author
  })
})

One or two routes may be fine, but any more than that and maintenance becomes tricky. Code becomes more complex and bugs are harder to find. Using Hono, the code is very neat.

It is also easy to handle bindings to Cloudflare products, such as KV, R2, D1, etc. as Hono uses a "context model". A context is a container that holds the application's state until a request is received, and a response is returned. You can use a context to retrieve a request object, set response headers, and create custom variables. It also holds Cloudflare bindings. For example, if you set up a Cloudflare KV namespace with the name MY_KV, you can access it as follows, with TypeScript type completion.

import { Hono } from 'hono'

type Env = {
  Bindings: {
    MY_KV: KVNamespace
  }
}

const app = new Hono<Env>()

app.post('/message', async (c) => {
  const message = c.req.query('message') ?? 'Hi'
  await c.env.MY_KV.put('message', message)
  return c.text(`message is set`, 201)
})

Hono lets you write code in a simple and intuitive way, but that doesn't mean there are limitations. You can do everything possible with Cloudflare Workers using Hono.

Add it when you want to use it

Hono is tiny. With the smallest preset, hono/tiny, you can write a "Hello World" application in just 12 KB. This is because it uses only the Web Standards API built into the runtime and has minimal functions. In comparison, the bundle size of Express is 579 KB.

However, there is much that you can do.

You can easily add functions using middleware. For example, it is a bit tedious to implement Basic Authentication from scratch, but with the built-in Basic Auth middleware, you can apply Basic Authentication to the path /auth/page with just this:

import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use(
  '/auth/*',
  basicAuth({
    username: 'hono',
    password: 'acoolproject',
  })
)

app.get('/auth/page', (c) => {
  return c.text('You are authorized')
})

Hono's package also includes built-in middleware that allows Bearer and JWT authentication, and easy configuration of CORS, etc. These built-in middleware components do not depend on external libraries, but there is also many 3rd-party middleware that allow the use of external libraries, such as authentication middleware using Clerk and Auth.js, and validators using Zod and Valibot.

There are also a number of built-in helpers, including the Streaming helper, which is useful for implementing AI. These can be added when you want to use them, and the file size increases only when they are added.

In Cloudflare Workers, there is a limit to a file size of a Worker. Keeping the core small and extending functions with middleware and helpers makes a lot of sense.

Onion structure

The important concepts of Hono are ”handler” and "middleware”.

A handler is a place to write a function that receives a request and returns a response, as specified by the user. For example, you can write a handler that gets a value of a query parameter, retrieves data from a database, and returns the result in JSON. Middleware can handle the requests that come to the handler and the responses that the handler returns. You can combine middleware with other middleware to build more large and complex applications. It is structured like an onion.

In a remarkably simple way, you can create middleware. For example, a custom logger that logs the request can be written as follows:

app.use(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.path}`)
  await next()
})

If you want to add a custom header to the response, write the following:

app.use(async (c, next) => {
  await next()
  c.header('X-Message', 'Hi, this is Hono!')
})

It would be interesting to combine this with HTMLRewriter. If an endpoint returns HTML, the middleware that modifies the HTML tags in it can be written as follows:

app.get('/pages/*', async (c, next) => {
  await next()

  class AttributeRewriter {
    constructor(attributeName) {
      this.attributeName = attributeName
    }
    element(element) {
      const attribute = element.getAttribute(this.attributeName)
      if (attribute) {
        element.setAttribute(this.attributeName, attribute.replace('oldhost', 'newhost'))
      }
    }
  }
  const rewriter = new HTMLRewriter().on('a', new AttributeRewriter('href'))

  const contentType = c.res.headers.get('Content-Type')

  if (contentType!.startsWith('text/html')) {
    c.res = rewriter.transform(c.res)
  }
})

There is very little to remember to create middleware. All you have to do is to work with the context, which you should already know.

The RPC is like magic

Hono has a strong type system. One feature that uses this is RPC (Remote Procedure Call). With RPC, you can express server-side API specifications as TypeScript types. When these types are loaded as generics in a client, the paths, arguments, and return types of each API endpoint are inferred. It's like magic.

For example, imagine an endpoint for creating a blog post. This endpoint takes a number type id and a string type title. Using Zod, one of the validator libraries that support TypeScript inference, you can define the schema like this:

import { z } from 'zod'

const schema = z.object({
  id: z.number(),
  title: z.string()
})

You create a handler that receives this object in JSON format via a POST request to the path /posts. Using Zod Validator, you check if it matches the schema. The response will have a property called message of type string.

import { zValidator } from '@hono/zod-validator'

const app = new Hono().basePath('/v1')

// ...

const routes = app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({
    message: `${data.id.toString()} is ${data.title}`
  })
})

This is a “typical” Hono handler. However, the TypeScript type you can get from the typeof for the routes will contain the information about its Web API specification. In this case, it includes the endpoint for creating blog posts — sending a POST request to the path /posts returns a JSON object.

export type AppType = typeof routes

Now, let's create a client. You pass the earlier AppType as generics to a Hono client object.

import { hc } from 'hono/client'
import { AppType } from '.'

const client = hc<AppType>('http://localhost:8787')

With this setup, you're ready. It's magic time.

Code completion works perfectly. When you write client-side code, you no longer need to know the API specifications completely, which also helps eliminate mistakes.

Server-side JSX is fun

Hono provides built-in JSX, a syntax that allows you to write code in JavaScript that looks like HTML tags. When you hear the term JSX, you may think of React, a front-end UI library. However, Hono's JSX was initially developed to run only on the server side. When we first started developing Hono, we were looking for template engines to render HTML. Most template engines, such as Handlebars and EJS, use eval internally and are incompatible with Cloudflare Workers, which does not support it. Then we came up with the idea of using JSX.

Hono's JSX is unique in that it treats the tags as a string. So the following strange code actually works.

console.log((<h1>Hello!</h1>).toString())

There is no need to do renderToString() as in React. If you want to render HTML, just return this as is.

app.get('/', (c) => c.html(<h1>Hello</h1>))

Very interesting is the creation of Suspense — a feature in React that allows you to display a fallback UI while waiting for an asynchronous component to load — without any client implementation. The asynchronous components are running in a server-only implementation.

Server-side JSX is a better developer experience than you might imagine. You can use the toolchains for React's JSX in the same way for Hono's JSX, including the ability to complete tags in the editor. They bring mature front-end technology to the server side.

Testing is important

Testing is important. Fortunately, you can write tests easily when using Hono.

For example, let's write a test for an endpoint. To test for a 200 response status of a request coming to / with the GET method, you can write the following:

it('should return 200 response', async () => {
  const res = await app.request('/')
  expect(res.status).toBe(200)
})

Simple, right? The beauty of this test is that you don't have to bring up the server. The Web Standard API black boxes the server layer. The internal tests of Hono have 20,000 lines of code, but most of them are written in the same style as above, without the server up and running. 

Going to full-stack

We released a new major version 4 in February 2024. There are three main features that stand out:

  1. Static site generation

  2. Client components

  3. File-based routing

With these features, you can create full-stack applications with a user interface in Hono.

The introduction of client components allows JSX to work in the client. Now you can add interactions to your pages. Static site generation allows you to create blogs, etc. without having to bundle them into a single JavaScript file. We have also started an experimental project called HonoX. This is a meta-framework using Hono and Vite that provides file-based routing and a mechanism to hydrate client-side components to server-side generated HTML. It is easier to create larger applications that are a great match for Cloudflare Pages or Workers.

In addition to that, plans are underway to run it as a base server for existing full-stack frameworks such as Remix and Qwik.

In contrast to the Next.js framework, which started from the client-side with React, Hono is trying to become a full-stack framework starting from the server-side.

Hono Conference

On June 22, 2024, I held the "Hono Conference" in Tokyo, the first event to consist entirely of Hono-focused talks. One hundred people attended, and the event was a great success.

It was my dream to do this event. Now, there are 200 contributors to the honojs/hono repository on GitHub. If you include other Hono related repositories, there are many more. Creating "the most invincible framework we could think of" is a lot of fun for contributors and users.

Below is a group photo taken at the end of the event. This is my treasure. I want to make the 2nd event a global event.

Hono is 炎

I haven't mentioned the origin of the name Hono yet. The name Hono is from the Japanese word for "". It is similar to the word "flare". Hono now runs on a variety of runtimes, but I said that it was first created to create Cloud"flare" Workers applications. It is an honor for Cloudflare that it has remained in its name.

That is all that the creator of Hono has to say about Hono.

Just try it

Everyone who has experienced application development with Hono and Cloudflare Workers says "the developer experience is a great experience". If you haven't experienced it yet, just try it.

See the Hono website for how to get started. If you are interested in reporting issues or contributing, please see the GitHub project. Plus, you can watch my interview about Hono on the YouTube Cloudflare Developers channel.

Cloudflare에서는 전체 기업 네트워크를 보호하고, 고객이 인터넷 규모의 애플리케이션을 효과적으로 구축하도록 지원하며, 웹 사이트와 인터넷 애플리케이션을 가속화하고, DDoS 공격을 막으며, 해커를 막고, Zero Trust로 향하는 고객의 여정을 지원합니다.

어떤 장치로든 1.1.1.1에 방문해 인터넷을 더 빠르고 안전하게 만들어 주는 Cloudflare의 무료 앱을 사용해 보세요.

더 나은 인터넷을 만들기 위한 Cloudflare의 사명을 자세히 알아보려면 여기에서 시작하세요. 새로운 커리어 경로를 찾고 있다면 채용 공고를 확인해 보세요.
Cloudflare WorkersCloudflare Pages (KO)

X에서 팔로우하기

Yusuke Wada|yusukebe
Cloudflare|@cloudflare

관련 게시물

2024년 10월 31일 오후 1:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network. ...

2024년 10월 25일 오후 1:00

Elephants in tunnels: how Hyperdrive connects to databases inside your VPC networks

Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for directing database traffic from Workers across Cloudflare Tunnels. We dive deep on what it took to add this feature....