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:
- Hono + JSX Middleware
- htmx
- Zod
- Tailwind CSS
- Cloudflare Workers
- Cloudflare D1
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.
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:
Build size:
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:
- It runs on the edges.
- I can use JavaScript/JSX.
- I can avoid spaghetti code by organizing the code well.
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: