Hono

Hono logo

Overview

Hono is a JavaScript HTTP server library that runs in any JavaScript runtime. This includes AWS Lambda, Bun, Cloudflare Pages, Cloudflare Workers, Deno, Fastly, Lagon, Netlify, NextJS, Node.js, and Vercel.

"Honō" is the Japanese word for "flame" or "blaze" which explains its logo.

Hono is significantly faster than Express which is the most popular HTTP library for Node.js.

Elysia is a competitor to Hono. It has slightly better performance than Hono, but only runs in Bun.

Projects

To create a new project that uses Hono, enter npm create hono@latest or bunx create-hono. This will prompt for:

It will then run for almost a minute. Why so long? When it finishes, the target directory will be created and will contain the following files:

Install the dependencies by entering npm install or bun install.

Run the project by entering npm run dev or bun dev.

By default, the server listens on port 3000. It also provides hot reloading.

Routes

To define routes, create a Hono object and call the methods get, post, put, patch, delete, and all on it. Each of these methods take a URL path and a function that is passed a Context object.

For example:

const app = new Hono();

app.get('/some/path', (c: Context) => {
...
});

Path parameters are described in URL path strings as a name preceded by a colon. Optional parameters are followed by ?. For example: '/population/:state/:year?'.

Path parameter names can also include a regular expression. For example: '/population/:state{[0-9]*}' TODO: When is this useful?

The callback functions passed to routes must return a Response object. This is typically done by returning the result of a call to c.text, c.json, or c.html.

It is also possible to override the route methods to simplify the code. For example instead of having the callback functions return a Response object, they could return a string (which could automatically use c.text), JSX (which could automatically use c.html), or any other kind of value (which could automatically use c.json). This idea is implemented in code found in the issue endpoints returning raw values.

Context

The Context object that is passed to routes has many properties and methods. The most useful properties are req (a HonoRequest object) and res (a Response object defined by the Fetch API).

The most useful methods on the c.req object related to extracting data from a request are described in the following table.

ActionCode
get value of request headerc.req.header('Some-Name')
get value of path parameterc.req.param('some-name')
get value of query parameterc.req.query('some-name')
get text from bodyconst text = await c.req.text();
get FormData from bodyconst formData = await c.req.formData();
get property from formDataconst value = (formData.get('property') as string) || '';
get JSON from bodyconst object = await c.req.json();

The most useful methods on the Context object related to creating a response are described in the following table.

ActionCode
set value of a response headerc.header('Some-Name', 'some value');
set status codec.status(someCode);
return text responsereturn c.text('some text');
return JSON responsereturn c.json(someObject);
return HTML responsereturn c.html(someHTML);
return "Not Found" errorreturn c.notFound();
return empty responsereturn c.body();
redirect to another URLreturn c.redirect('someURL');

In the c.html method, someHTML is a string of HTML or JSX.

See the endpoints returning raw values proposal.

To share values between routes, set them in one route with c.set('someName', 'some value'); and get them other routes with c.get('someName').

JSX

Hono supports using JSX to generate HTML responses. This is useful in conjunction with htmx.

IMPORTANT: Source files that use JSX must have a file extension of .tsx.

Verify that the following compilerOptions properties are present in tsconfig.json. They are by default in new Hono projects.

"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"

Remove the jsxFragmentFactory property if it is present.

Routers

Hono includes five routers. Typically the default router is fine and this information can be ignored.

The provided routers are:

The default router is SmartRouter configured to select between RegExpRouter and TrieRouter.

To use a different router, pass one to the Hono constructor. For example:

import {Hono} from 'hono';
import {LinearRouter} from 'hono/router/linear-router';

const app = new Hono({router: new LinearRouter()});

Hono provides presets for selecting a router based on how Hono is imported. For example, import {Hono} from '{somewhere}';

Import Hono FromRouter Used
'hono'SmartRouter selecting RegExpRouter or TrieRouter
'hono/quick'SmartRouter selecting LinearRouter or TrieRouter
'hono/tiny'PatternRouter

CRUD Example

This example demonstrates implementing endpoints for CRUD operations on a collection of dogs. This code does not require any additional packages to be installed.

Hono dog app

src/index.ts

import {Hono} from 'hono';
import {serveStatic} from 'hono/bun';
import {logger} from 'hono/logger';
import dogRouter from './dog-router';

const app = new Hono();

// This logs all HTTP requests to the terminal where the server is running.
app.use('/*', logger());

// This serves static files from the public directory.
app.use('/*', serveStatic({root: './public'}));

app.route('/dog', dogRouter);

export default app;

The port on which the server listens defaults to 3000. To listen on a different port, say 1234, change the last line to the following.

export default {port: 1234, fetch: app.fetch};

public/styles.css

body {
font-family: sans-serif;
}

src/dog-router.tsx

import {Context, Hono} from 'hono';
import type {FC} from 'hono/jsx';

const router = new Hono();

interface NewDog {
name: string;
breed: string;
}

interface Dog extends NewDog {
id: number;
}

let lastId = 0;

// The dogs are maintained in memory.
const dogMap: {[id: number]: Dog} = {};

function addDog(name: string, breed: string): Dog {
const id = ++lastId;
const dog = {id, name, breed};
dogMap[id] = dog;
return dog;
}

addDog('Comet', 'Whippet');
addDog('Oscar', 'German Shorthaired Pointer');

// This provides HTML boilerplate for any page.
const Layout: FC = props => {
return (
<html>
<head>
<link rel="stylesheet" href="/styles.css" />
<title>{props.title}</title>
</head>
<body>{props.children}</body>
</html>
);
};

// This returns JSX for a page that
// list the dogs passed in a prop.
const DogPage: FC = ({dogs}) => {
const title = 'Dogs I Know';
return (
<Layout title={title}>
<h1>{title}</h1>
<ul>
{dogs.map((dog: Dog) => (
<li>
{dog.name} is a {dog.breed}.
</li>
))}
</ul>
</Layout>
);
};

// This gets all the dogs as either JSON or HTML.
router.get('/', (c: Context) => {
const accept = c.req.header('Accept');
if (accept && accept.includes('application/json')) {
return c.json(dogMap);
}

const dogs = Object.values(dogMap).sort((a, b) =>
a.name.localeCompare(b.name)
);
return c.html(<DogPage dogs={dogs} />);
});

// This gets one dog by its id as JSON.
router.get('/:id', (c: Context) => {
const id = Number(c.req.param('id'));
const dog = dogMap[id];
c.status(dog ? 200 : 404);
return c.json(dog);
});

// This creates a new dog.
router.post('/', async (c: Context) => {
const data = (await c.req.json()) as unknown as NewDog;
const dog = addDog(data.name, data.breed);
c.status(201);
return c.json(dog);
});

// This updates the dog with a given id.
router.put('/:id', async (c: Context) => {
const id = Number(c.req.param('id'));
const data = (await c.req.json()) as unknown as NewDog;
const dog = dogMap[id];
if (dog) {
dog.name = data.name;
dog.breed = data.breed;
}
c.status(dog ? 200 : 404);
return c.json(dog);
});

// This deletes the dog with a given id.
router.delete('/:id', async (c: Context) => {
const id = Number(c.req.param('id'));
const dog = dogMap[id];
if (dog) delete dogMap[id];
c.status(dog ? 200 : 404);
return c.text('');
});

export default router;

src/index.test.ts

This tests all the CRUD functionality. To run all the tests, enter bun test.

import {describe, expect, it} from 'bun:test';
import app from '.';

const comet = {id: 1, name: 'Comet', breed: 'Whippet'};

const URL_PREFIX = 'http://localhost:3000/dog';

async function getAllDogs() {
const req = new Request(URL_PREFIX);
req.headers.set('Accept', 'application/json');
const res = await app.fetch(req);
return res.json();
}

async function getDogById(id: number) {
const req = new Request(`${URL_PREFIX}/${id}`);
const res = await app.fetch(req);
return res.status === 200 ? res.json() : undefined;
}

describe('dog endpoints', () => {
it('should get all dogs', async () => {
const dogs = await getAllDogs();
expect(Object.keys(dogs).length).toBe(2);
expect(dogs[1]).toEqual(comet);
});

it('should get one dog', async () => {
const req = new Request(URL_PREFIX + '/1');
const res = await app.fetch(req);
const dog = await res.json();
expect(dog).toEqual(comet);
expect(res.status).toBe(200);
});

it('should create a dog', async () => {
const dog = {
name: 'Ramsay',
breed: 'Native American Indian Dog'
};
const req = new Request(URL_PREFIX, {
method: 'POST',
body: JSON.stringify(dog),
headers: {
'Content-Type': 'application/json'
}
});
const res = await app.fetch(req);

expect(res.status).toBe(201);
const newDog = await res.json();

// Verify that the dog was added.
const dogs = await getAllDogs();
const foundDog = dogs[newDog.id];
expect(newDog.name).toBe(foundDog.name);
expect(newDog.breed).toBe(foundDog.breed);
});

it('should update a dog', async () => {
const dog = {
name: 'Fireball',
breed: 'Greyhound'
};
const req = new Request(URL_PREFIX + '/1', {
method: 'PUT',
body: JSON.stringify(dog),
headers: {
'Content-Type': 'application/json'
}
});
const res = await app.fetch(req);

expect(res.status).toBe(200);
const updatedDog = await res.json();
expect(updatedDog.name).toBe(dog.name);
expect(updatedDog.breed).toBe(dog.breed);

// Verify that the dog was updated.
const dogs = await getAllDogs();
const foundDog = dogs[updatedDog.id];
expect(updatedDog.name).toBe(foundDog.name);
expect(updatedDog.breed).toBe(foundDog.breed);
});

it('should delete a dog', async () => {
const id = 1;
const req = new Request(`${URL_PREFIX}/${id}`, {
method: 'DELETE'
});
const res = await app.fetch(req);

expect(res.status).toBe(200);

// Verify that the dog was deleted.
const dog = await getDogById(id);
expect(dog).toBeUndefined();
const dogs = await getAllDogs();
expect(dogs[id]).toBeUndefined();
});
});

File-based Routing

Hono does not support file-based routing, but the addon package honox does.

The following steps demonstrate using file-based routing with honox.

  1. Create a new project by entering bun create hono@latest.

    For the template type, choose "bun".

  2. cd to the new project directory.

  3. Enter bun install.

  4. Enter bun add honox.

  5. Enter bun add -d vite.

  6. Create the file vite.config.ts containing the following:

    import honox from 'honox/vite';
    import {defineConfig} from 'vite';

    export default defineConfig({
    plugins: [honox()]
    });
  7. Rename the src directory to app.

  8. Renamed app/index.ts to app/server.ts.

  9. Replace the contents of app/server.ts with the following:

    import {showRoutes} from 'hono/dev';
    import {createApp} from 'honox/server';

    const app = createApp();
    showRoutes(app);
    export default app;
  10. Edit package.json.

    Change src/index.ts to app/server.ts in the "dev" script.

    TODO: Maybe this needs to use the vite command.

  11. Create the directory src/routes.

  12. Inside the new directory, create the files _404.tsx, _error.tsx, _renderer.tsx, and index.tsx.

  13. Add the following in src/routes/_404.tsx:

    import {NotFoundHandler} from 'hono';

    const handler: NotFoundHandler = c => {
    return c.render(<h1>Sorry, Not Found...</h1>);
    };

    export default handler;
  14. Add the following in src/routes/_error.tsx:

    import {ErrorHandler} from 'hono';

    const handler: ErrorHandler = (e, c) => {
    return c.render(<h1>Error! {e.message}</h1>);
    };

    export default handler;
  15. Add the following in src/routes/_renderer.tsx:

    import {jsxRenderer} from 'hono/jsx-renderer';

    export default jsxRenderer(({children}) => {
    return (
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta
    name="viewport"
    content="width=device-width, initial-scale=1.0"
    />
    </head>
    <body>{children}</body>
    </html>
    );
    });
  16. Add the following in src/routes/index.tsx:

    export default function Home() {
    return <h1>Welcome!</h1>;
    }
  17. Start the server by entering bun dev.

    This gives me the error described in this issue.

  18. Browse localhost:3000.

Validation

Hono supports validating the following kinds of request data passed to endpoints:

When validation fails, the response status code will be 400, indicating a bad request, and the response body will be a JSON object similar to the following that describes the validation error:

{
"success": false,
"error": {
"issues": [
{
"message": "{description-of-error}",
"path": ["{name-of-invalid-property}"]
... more properties describing the error ...
}
],
"name": "ZodError"
}
}

The support for specifying validation criteria is quite basic. However, integration with Zod, can be installed for much better support.

To install a library that integrates Zod validation with Hono, enter npm install @hono/zod-validator or bun add @hono/zod-validator.

To make this library available in code, add the following imports:

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

To add validation to a route, add any number of calls to zValidator as additional arguments after the route path and before the handler function. Each call can validate a different kind of request data. For example, one can be for validating path parameters and another can be for validating the JSON request body.

The following code adds Zod validation to the dog endpoints that were defined earlier. Look for lines that contain "Schema" or "Validator".

This code also captures the route objects and exports their types so they can be used in client code to call the endpoints using the RPC approach described in the next section.

src/dog-router.tsx Improved

import {Context, Hono} from 'hono';
import type {FC} from 'hono/jsx';
import {z} from 'zod';
import {zValidator} from '@hono/zod-validator';

const router = new Hono();

interface NewDog {
name: string;
breed: string;
}

interface Dog extends NewDog {
id: number;
}

let lastId = 0;

// The dogs are maintained in memory.
const dogMap: {[id: number]: Dog} = {};

function addDog(name: string, breed: string): Dog {
const id = ++lastId;
const dog = {id, name, breed};
dogMap[id] = dog;
return dog;
}

addDog('Comet', 'Whippet');
addDog('Oscar', 'German Shorthaired Pointer');

// This provides HTML boilerplate for any page.
const Layout: FC = props => {
return (
<html>
<head>
<link rel="stylesheet" href="/styles.css" />
<title>{props.title}</title>
</head>
<body>{props.children}</body>
</html>
);
};

// This returns JSX for a page that
// list the dogs passed in a prop.
const DogPage: FC = ({dogs}) => {
const title = 'Dogs I Know';
return (
<Layout title={title}>
<h1>{title}</h1>
<ul>
{dogs.map((dog: Dog) => (
<li>
{dog.name} is a {dog.breed}.
</li>
))}
</ul>
</Layout>
);
};

// This gets all the dogs as either JSON or HTML.
const getAllRoute = router.get('/', (c: Context) => {
const accept = c.req.header('Accept');
if (accept && accept.includes('application/json')) {
return c.json(dogMap);
}

const dogs = Object.values(dogMap).sort((a, b) =>
a.name.localeCompare(b.name)
);
return c.html(<DogPage dogs={dogs} />);
});

// This gets one dog by its id as JSON.
const idSchema = z.object({
id: z.coerce.number().positive()
});
const idValidator = zValidator('param', idSchema);
const getOneRoute = router.get('/:id', idValidator, (c: Context) => {
const id = Number(c.req.param('id'));
const dog = dogMap[id];
c.status(dog ? 200 : 404);
return c.json(dog);
});

// This creates a new dog.
const dogSchema = z
.object({
id: z.number().positive().optional(),
name: z.string().min(1),
breed: z.string().min(2)
})
.strict(); // no extra properties allowed
const dogValidator = zValidator('json', dogSchema);
const createRoute = router.post('/', dogValidator, async (c: Context) => {
const data = (await c.req.json()) as unknown as NewDog;
const dog = addDog(data.name, data.breed);
c.status(201);
return c.json(dog);
});

// This updates the dog with a given id.
const updateRoute = router.put(
'/:id',
idValidator,
dogValidator,
async (c: Context) => {
const id = Number(c.req.param('id'));
const data = (await c.req.json()) as unknown as NewDog;
const dog = dogMap[id];
if (dog) {
dog.name = data.name;
dog.breed = data.breed;
}
c.status(dog ? 200 : 404);
return c.json(dog);
}
);

// This deletes the dog with a given id.
const deleteRoute = router.delete('/:id', idValidator, async (c: Context) => {
const id = Number(c.req.param('id'));
const dog = dogMap[id];
if (dog) delete dogMap[id];
c.status(dog ? 200 : 404);
return c.text('');
});

export default router;
export type CreateType = typeof createRoute;
export type DeleteType = typeof deleteRoute;
export type GetAllType = typeof getAllRoute;
export type GetOneType = typeof getOneRoute;
export type UpdateType = typeof updateRoute;

RPC

Hono can export the type definitions for routes that are based on the specified validators. These definitions can be used in client code to add type checking to endpoint calls that are made using the Hono client.

Route types were exported in the code in the previous section.

The following is an example of client code that uses the Hono client.

TODO: Why don't I get any type information in VS Code when using these routes?

import {CreateType, DeleteType, GetAllType, UpdateType} from './dog-router';
import {hc} from 'hono/client';

const URL_PREFIX = 'http://localhost:3000/';

const createClient = hc<CreateType>(URL_PREFIX);
const deleteClient = hc<DeleteType>(URL_PREFIX);
const getAllClient = hc<GetAllType>(URL_PREFIX);
const updateClient = hc<UpdateType>(URL_PREFIX);

async function demo() {
// Create a dog.
let res = await createClient.dog.$post(
{
json: {
name: 'Ramsay',
breed: 'Native American Indian Dog'
}
},
{
headers: {
Accept: 'application/json'
}
}
);

// Update a dog.
res = await updateClient.dog[':id'].$put({
param: {id: 1},
json: {
// id: 1,
name: 'Fireball',
breed: 'Greyhound'
}
});

// Delete a dog.
res = await deleteClient.dog[':id'].$delete({param: {id: 2}});

// Get all the dogs.
res = await getAllClient.dog.$get(
{},
{
headers: {
Accept: 'application/json'
}
}
);
const dogs = await res.json();
console.log('dogs =', dogs);
}

demo();

The output from the code above is:

dogs = {
"1": {
id: 1,
name: "Fireball",
breed: "Greyhound",
},
"3": {
id: 3,
name: "Ramsay",
breed: "Native American Indian Dog",
},
}

For more detail, see RPC/Client.

Tests

The following code in the file src/index.test.ts tests all the routes defined above. To run it, enter bun test.

import {describe, expect, it} from 'bun:test';
import app from '.';

const comet = {id: 1, name: 'Comet', breed: 'Whippet'};

const URL_PREFIX = 'http://localhost:3000/dog';

async function getAllDogs() {
const req = new Request(URL_PREFIX);
req.headers.set('Accept', 'application/json');
const res = await app.fetch(req);
return res.json();
}

async function getDogById(id: number) {
const req = new Request(`${URL_PREFIX}/${id}`);
const res = await app.fetch(req);
return res.status === 200 ? res.json() : undefined;
}

describe('dog endpoints', () => {
it('should get all dogs', async () => {
const dogs = await getAllDogs();
expect(Object.keys(dogs).length).toBe(2);
expect(dogs[1]).toEqual(comet);
});

it('should get one dog', async () => {
const req = new Request(URL_PREFIX + '/1');
const res = await app.fetch(req);
const dog = await res.json();
expect(dog).toEqual(comet);
expect(res.status).toBe(200);
});

it('should create a dog', async () => {
const dog = {
name: 'Ramsay',
breed: 'Native American Indian Dog'
};
const req = new Request(URL_PREFIX, {
method: 'POST',
body: JSON.stringify(dog),
headers: {
'Content-Type': 'application/json'
}
});
const res = await app.fetch(req);

expect(res.status).toBe(201);
const newDog = await res.json();

// Verify that the dog was added.
const dogs = await getAllDogs();
const foundDog = dogs[newDog.id];
expect(newDog.name).toBe(foundDog.name);
expect(newDog.breed).toBe(foundDog.breed);
});

it('should update a dog', async () => {
const dog = {
name: 'Fireball',
breed: 'Greyhound'
};
const req = new Request(URL_PREFIX + '/1', {
method: 'PUT',
body: JSON.stringify(dog),
headers: {
'Content-Type': 'application/json'
}
});
const res = await app.fetch(req);

expect(res.status).toBe(200);
const updatedDog = await res.json();
expect(updatedDog.name).toBe(dog.name);
expect(updatedDog.breed).toBe(dog.breed);

// Verify that the dog was updated.
const dogs = await getAllDogs();
const foundDog = dogs[updatedDog.id];
expect(updatedDog.name).toBe(foundDog.name);
expect(updatedDog.breed).toBe(foundDog.breed);
});

it('should delete a dog', async () => {
const id = 1;
const req = new Request(`${URL_PREFIX}/${id}`, {
method: 'DELETE'
});
const res = await app.fetch(req);

expect(res.status).toBe(200);

// Verify that the dog was deleted.
const dog = await getDogById(id);
expect(dog).toBeUndefined();
const dogs = await getAllDogs();
expect(dogs[id]).toBeUndefined();
});
});

Performance

The following sites provide relevant benchmarks: