# Hono: a modern web framework for JavaScript

> Hono is a lightweight JavaScript web framework built on Web Standards. Create an app with Bun, handle routing, read request bodies, and send responses.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-08 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/hono/

Hono is a modern, lightweight web framework for JavaScript. It's the second post in my four-part mini-series on [Bun](https://flaviocopes.com/bun/) and Hono.

I like Hono because it feels familiar if you know [Express](https://flaviocopes.com/express/), but it's built on Web Standards from the ground up. The request and response objects are the same [Fetch API](https://flaviocopes.com/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:

```bash
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:

```bash
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 `/`:

```js
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()`:

```js
app.get('/', c => { ... })
app.post('/', c => { ... })
app.put('/', c => { ... })
app.delete('/', c => { ... })
```

Each method takes a handler function. Use `async` when you need `await`:

```js
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:

```js
app.get('/', (c) => c.text('Hello, World!'))
```

Or with an explicit return when you need more logic:

```js
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:

```js
app.get('/', (c) => { ... })
```

### Named parameters

To capture a value from the URL path, use named parameters:

```js
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()`:

```js
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()`:

```js
app.post('/', async c => {
  const body = await c.req.text()
})
```

For JSON, use `await c.req.json()`:

```js
app.post('/', async c => {
  const body = await c.req.json()
})
```

For form data, use `await c.req.parseBody()`:

```js
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:

```js
(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:

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

Use `c.html()` to send an HTML string with `Content-Type: text/html`:

```js
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:

```js
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:

```js
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.
