Building Full-Stack Apps with Hono
Hono is a small, simple, and ultrafast web framework for building applications on the edge. Originally designed for Cloudflare Workers, it now supports Deno, Bun, and Node.js, making it a versatile choice for modern full‑stack development. With a built-in router, middleware system, and first-class TypeScript support, Hono enables you to create both API backends and server-rendered frontends with minimal boilerplate.
What is Hono?
Hono (Japanese for "framework") is a lightweight HTTP framework that focuses on performance and developer experience. It provides a clean, Express‑like API but with zero dependencies and a tiny footprint (under 10 KB). Key features include:
- Fast routing based on Trie trees
- Built‑in middleware for CORS, compression, authentication, and more
- Support for streaming, WebSockets, and server‑side rendering
- Seamless deployment to Cloudflare Workers, Deno, Bun, or Node.js
- TypeScript-first design with excellent type inference
Why Use Hono for Full-Stack Apps?
Full‑stack applications require both an API and a frontend. Hono excels because it can handle both sides efficiently:
- Unified codebase – Write your backend routes and serve static files or rendered pages from the same runtime.
- Edge performance – Deploy to Cloudflare Workers for near‑zero latency globally.
- Minimal overhead – No heavy frameworks, just the essentials.
- Easy integration – Works with any database (via adapters), ORMs like Prisma, and authentication libraries.
- Modern JavaScript – Supports ES modules, async/await, and native fetch API.
Getting Started
Install Hono via your preferred package manager. The following examples use Node.js with npm, but the same code works on Deno and Bun.
npm install hono
Create a basic server:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
For Node.js, you need a server adapter. Hono provides @hono/node-server:
npm install @hono/node-server
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Hono on Node!'))
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`)
})
Building a Full-Stack Application
We'll build a simple note‑taking app with a JSON API and a static frontend that uses hono/serve-static and hono/jsx for server‑side rendering.
1. Project Structure
my-app/
├── src/
│ ├── index.ts # Entry point
│ ├── routes/
│ │ ├── notes.ts # API routes
│ │ └── pages.ts # SSR routes
│ └── db.ts # Simple in‑memory store
├── public/
│ └── index.html # Fallback static file
└── package.json
2. Database (In-Memory for Demo)
// src/db.ts
type Note = {
id: string
title: string
content: string
createdAt: string
}
const notes: Note[] = []
export const getNotes = () => notes
export const addNote = (title: string, content: string): Note => {
const note: Note = {
id: crypto.randomUUID(),
title,
content,
createdAt: new Date().toISOString(),
}
notes.push(note)
return note
}
export const deleteNote = (id: string): boolean => {
const index = notes.findIndex(n => n.id === id)
if (index === -1) return false
notes.splice(index, 1)
return true
}
3. API Routes
// src/routes/notes.ts
import { Hono } from 'hono'
import { getNotes, addNote, deleteNote } from '../db'
const notesApp = new Hono()
notesApp.get('/', (c) => {
const allNotes = getNotes()
return c.json(allNotes)
})
notesApp.post('/', async (c) => {
const { title, content } = await c.req.json()
if (!title || !content) {
return c.json({ error: 'Title and content required' }, 400)
}
const note = addNote(title, content)
return c.json(note, 201)
})
notesApp.delete('/:id', (c) => {
const id = c.req.param('id')
const deleted = deleteNote(id)
if (!deleted) {
return c.json({ error: 'Note not found' }, 404)
}
return c.json({ message: 'Deleted' })
})
export default notesApp
4. Server‑Side Rendering with JSX
Hono has a built‑in JSX engine. Create a simple page that lists notes.
// src/routes/pages.ts
import { Hono } from 'hono'
import { getNotes } from '../db'
const pagesApp = new Hono()
pagesApp.get('/', (c) => {
const notes = getNotes()
return c.html(
<html>
<head><title>Notes App</title></head>
<body>
<h1>My Notes</h1>
<ul>
{notes.map(note => (
<li>
<strong>{note.title}</strong> – {note.content}
</li>
))}
</ul>
</body>
</html>
)
})
export default pagesApp
To use JSX, add "jsx": "react-jsx" to your tsconfig.json (or use .tsx files). Hono also supports hono/html for template literals if you prefer.
5. Static Files and Middleware
Serve static files (CSS, client-side JS) from the public directory.
// src/index.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { serveStatic } from '@hono/node-server/serve-static'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import notesApp from './routes/notes'
import pagesApp from './routes/pages'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors()) // Allow CORS for API
// Mount sub‑routers
app.route('/api/notes', notesApp)
app.route('/pages', pagesApp)
// Serve static files from 'public'
app.use('/static/*', serveStatic({ root: './public' }))
// Fallback to index.html for client‑side routing
app.get('*', serveStatic({ path: './public/index.html' }))
serve(app, (info) => {
console.log(`Server running on http://localhost:${info.port}`)
})
The public/index.html file can contain a simple client‑side app that fetches from /api/notes.
Best Practices
- Use middleware wisely – Hono’s middleware chain is efficient. Place global middleware (logging, CORS) early, and route‑specific middleware inside sub‑routers.
- Validate inputs – Use libraries like
zodto validate request bodies and params. Hono’sc.req.validworks with validation middleware. - Structure by feature – Group routes, services, and models into modules. Use Hono’s
app.route()to mount sub‑apps. - Handle errors gracefully – Create a global error handler using
app.onError()to return consistent JSON or HTML responses. - Use environment variables – Access secrets via
c.env(on Cloudflare Workers) orprocess.env(Node). Avoid hard‑coding. - Optimize for the edge – When deploying to Workers, minimize dependencies and use Hono’s built‑in streaming for large payloads.
- Type everything – Define types for requests, responses, and database models. Hono provides strong typing for context, params, and queries.
Deployment
Hono apps can be deployed to multiple platforms:
- Cloudflare Workers – Use
wrangler. Export the app as default. - Deno Deploy – Run with
deno runusing thehonomodule fromnpm:specifier. - Bun –
bun run index.tswith the built‑in HTTP server. - Node.js – Use
@hono/node-serverand deploy via any Node hosting (Heroku, Railway, etc.).
Conclusion
Hono provides a refreshingly simple yet powerful foundation for building full‑stack applications. Its minimal footprint, blazing speed, and broad platform support make it an excellent choice for developers who want to focus on logic rather than configuration. By combining Hono’s routing, middleware, and SSR capabilities, you can create modern APIs and interactive frontends with ease. Whether you are building a small prototype or a production‑grade edge application, Hono gives you the tools to do it efficiently and cleanly. Start with a simple CRUD example, then expand with authentication, database integrations, and client‑side frameworks—all while keeping your codebase lean and maintainable.