Blog - Yusuke Wada

Screenshot

Hono + htmx + Cloudflare is a new stack


Hono + htmx + Cloudflare is a new stack

As a former backend engineer, I sometimes find React complex. Moreover, as a framework developer, creating a hydration mechanism can be troublesome. But we often end up using React.

One of the main advantages of using React is JSX. At first, JSX seems strange - “Why are HTML tags in JavaScript!!!” However, once I get used to it, I find that JSX is flexible and comfortable to use.

Today, I’ll introduce a tech stack where the main point is using JSX purely as a server-side template engine. This means we can use JSX without React.

Hono JSX Middleware

Hono - a JavaScript framework for edges - includes JSX middleware. You can write HTML with JSX but it’s strictly for server-side rendering, not for the client. As such, it serves as a template engine, much like Handlebars, EJS, mustache, or others.

const app = new Hono()

app.get('/', (c) => {
  return c.html(<h1>Hello!</h1>)
})

A Hono app can run on edge servers like Cloudflare Workers, Fastly Compute@Edge, or Deno Deploy. This allows for incredibly fast server-side rendering. Moreover, it doesn’t perform “hydration” for JavaScript, meaning you don’t lose user experience without the need for SPA transition. This combination of edge-based SSR and the absence of hydration makes for a very speedy setup.

htmx

htmx is a library that enables Ajax without the need to write JavaScript.

<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
  Click Me
</button>

It’s comparable to Hotwire, which is used by Ruby on Rails. However, unlike using React with a REST API, htmx can easily integrate with server-side JSX, making it simpler to create interactive experiences.

The stack

The entire stack includes the following components:

Cloudflare D1 is a database service that runs SQLite on Cloudflare edges. Although it’s currently in “alpha” status and not recommended for production use, it’s already fast and perfectly suitable for Proof of Concept (PoC) projects.

In the example below, I use Zod to validate incoming values. Hono’s Zod Validator Middleware is incredibly useful as it integrates with Hono, allowing us to easily get the types of validated values.

Screenshot

html.js

I must express my gratitude. This idea is based on @dctanner’s tweet. He named it the ”html.js” stack. You can find it in this repository:

https://github.com/dctanner/htmljs-todo-example

100 lines Todo app

It’s amazing. I was able to create a real Todo App example that inserts and deletes data in D1 SQLite on the edge with just 100 lines of code. It’s fast (~100ms) and lightweight (gzipped worker size: 22 KB)!

Here is the demo:

output

Build size:

Screenshot

Code

Normally, when I have to show an example code, I have to choose specific parts of the code and paste a few lines. However, this example is just 100 lines, so I’ll show the entire code.

comonent.tsx:

import { html } from 'hono/html'

export const Layout = (props: { children: any }) => html`
  <!DOCTYPE html>
  <html>
    <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <script src="https://unpkg.com/[email protected]"></script>
      <script src="https://unpkg.com/[email protected]"></script>
      <script src="https://cdn.tailwindcss.com"></script>
      <title>Hono + htmx</title>
    </head>
    <body>
      <div class="p-4">
        <h1 class="text-4xl font-bold mb-4"><a href="/">Todo</a></h1>
        ${props.children}
      </div>
    </body>
  </html>
`

export const AddTodo = () => (
  <form hx-post="/todo" hx-target="#todo" hx-swap="beforebegin" _="on htmx:afterRequest reset() me" class="mb-4">
    <div class="mb-2">
      <input name="title" type="text" class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg p-2.5" />
    </div>
    <button class="text-white bg-blue-700 hover:bg-blue-800 rounded-lg px-5 py-2 text-center" type="submit">
      Submit
    </button>
  </form>
)

export const Item = ({ title, id }: { title: string; id: string }) => (
  <p
    hx-delete={`/todo/${id}`}
    hx-swap="outerHTML"
    class="flex row items-center justify-between py-1 px-4 my-1 rounded-lg text-lg border bg-gray-100 text-gray-600 mb-2"
  >
    {title}
    <button class="font-medium">Delete</button>
  </p>
)

index.tsx:

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

import { Layout, AddTodo, Item } from './components'

type Bindings = {
  DB: D1Database
}

type Todo = {
  title: string
  id: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.get('/', async (c) => {
  const { results } = await c.env.DB.prepare(`SELECT id, title FROM todo;`).all<Todo>()
  const todos = results as unknown as Todo[] // Currently, should fix a type mismatch.
  return c.html(
    <Layout>
      <AddTodo />
      {todos.map((todo) => {
        return <Item title={todo.title} id={todo.id} />
      })}
      <div id="todo"></div>
    </Layout>
  )
})

app.post(
  '/todo',
  zValidator(
    'form',
    z.object({
      title: z.string().min(1)
    })
  ),
  async (c) => {
    const { title } = c.req.valid('form')
    const id = crypto.randomUUID()
    await c.env.DB.prepare(`INSERT INTO todo(id, title) VALUES(?, ?);`).bind(id, title).run()
    return c.html(<Item title={title} id={id} />)
  }
)

app.delete('/todo/:id', async (c) => {
  const id = c.req.param('id')
  await c.env.DB.prepare(`DELETE FROM todo WHERE id = ?;`).bind(id).run()
  c.status(200)
  return c.body(null)
})

export default app

Isn’t it elegant?

You can find the entire project here:

https://github.com/yusukebe/hono-htmx

Am I talking about PHP?

Perhaps, you’re wondering:

Are you talking about PHP?

To which I’ll answer:

No. But it’s quite similar!

It really feels like PHP, or perhaps Ruby on Rails. But I think PHP is not bad. Moreover, this stack has several advantages for me:

As I mentioned at the beginning, I used to be a backend engineer, so this approach to website creation is more familiar and comfortable for me. It’s simple and clean.

Going forward

There are a few things we need to work on to stabilize this stack. One is enabling file-based routing. Also, I’m not sure if using Hono’s JSX middleware is the best approach, maybe Preact would be a better choice.

Anyway, this stack has a nostalgic yet new feeling to it. Oh, I’ve forgotten one thing we need to do. We should name this stack!

Thanks

Again, thank you to @dctanner for the inspiring idea. I also recommend checking out his repository:

https://github.com/dctanner/htmljs-todo-example