Hono: a modern web framework for JavaScript
By Flavio Copes
Hono is a lightweight JavaScript web framework built on Web Standards. Create an app with Bun, handle routing, read request bodies, and send responses.
Hono is a modern, lightweight web framework for JavaScript. It’s the second post in my four-part mini-series on Bun and Hono.
I like Hono because it feels familiar if you know Express, but it’s built on Web Standards from the ground up. The request and response objects are the same Fetch API types you already use in the browser. Handlers are async-first. And the same app runs on Bun, Node.js, Deno, and Cloudflare Workers without rewriting your code.
It’s also tiny. The core package is a few kilobytes, and the router is fast enough for edge runtimes where every millisecond counts.
Create your first app
Hono supports many runtimes. This example uses Bun, since we covered it yesterday. From your projects folder, run:
bun create hono@latest my-app
On Node.js, use npm create hono@latest my-app instead. The CLI asks which template you want — pick bun for this walkthrough. Other templates cover Cloudflare Workers, Vercel, Deno, and more.
Then install dependencies and start the dev server:
cd my-app
bun install
bun run dev
Open http://localhost:3000 in your browser. You should see the default Hello World response.
The basic shape of a Hono app
Open src/index.ts. The starter imports Hono, creates an app with new Hono(), and registers a handler for GET /:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
With Bun you export the app and Bun runs it. There’s a method for every HTTP verb: get(), post(), put(), delete():
app.get('/', c => { ... })
app.post('/', c => { ... })
app.put('/', c => { ... })
app.delete('/', c => { ... })
Each method takes a handler function. Use async when you need await:
app.get('/', async c => { ... })
The handler receives a context object, usually named c. From it you access the request with c.req and build the response you return. c.req is a standard Request (URL, method, headers, body). The helpers like c.text() and c.json() wrap the standard Response object.
This works as a one-liner:
app.get('/', (c) => c.text('Hello, World!'))
Or with an explicit return when you need more logic:
app.get('/', (c) => {
return c.text('Hello, World!')
})
Be careful here: if you forget to return the response, the client gets a 404.
Routing
Routing maps a URL and HTTP method to a handler. In the Hello World example we had:
app.get('/', (c) => { ... })
Named parameters
To capture a value from the URL path, use named parameters:
app.get('/uppercase/:val', (c) => {
return c.text(c.req.param('val').toUpperCase())
})
A request to /uppercase/test returns TEST in the response body. You can use multiple named parameters in one route — c.req.param() returns an object with all of them.
Query strings
For query string values, use c.req.query():
app.get('/search', (c) => {
const q = c.req.query('q')
return c.text(`You searched for: ${q}`)
})
A request to /search?q=hono returns You searched for: hono.
Reading the request body
Common request body types are text/plain, application/json, application/x-www-form-urlencoded, and multipart/form-data.
For plain text, read the body with await c.req.text():
app.post('/', async c => {
const body = await c.req.text()
})
For JSON, use await c.req.json():
app.post('/', async c => {
const body = await c.req.json()
})
For form data, use await c.req.parseBody():
app.post('/', async c => {
const body = await c.req.parseBody()
})
c.req also exposes .path, .method, .url, .header(), and everything else from the standard Request object. You only get one shot at reading the body, so pick the right parser for the content type.
Sending responses
In the Hello World example we used c.text() to send a plain string:
(c) => c.text('Hello, World!')
This sets Content-Type to text/plain and sends the string you pass in.
Use c.json() to send an object or array as JSON:
app.get('/', (c) => c.json({ ok: true }))
Use c.html() to send an HTML string with Content-Type: text/html:
app.get('/', (c) => {
return c.html('<h1>Hello</h1>')
})
To return a 404, use c.notFound(). For other status codes, pass the number as the second argument:
app.get('/missing', (c) => c.notFound())
app.get('/gone', (c) => {
return c.text('Gone', 410)
})
Running on Node.js
The example above uses Bun. On Node.js, pick the nodejs template when you run create-hono, then import serve from @hono/node-server and call it with your app:
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
serve(app)
Run npm run dev and you’re set.
What’s next
That’s the core of Hono: create an app, register routes, read the request, return a response. The next posts in this series cover middleware and deploying to Cloudflare Workers.
Related posts about js: