03: Hono
Sample app
Hono is a great stand-in for express when using Bun. To get started, run bun install hono
A sample app looks like this:
- Write this code. Yes, you need the
export default app
to work.
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
// 1. create app
const app = new Hono();
// 2. Serve any static files
app.use(
"*",
serveStatic({
root: "./dist",
})
);
app.get("*", (c) => c.html("./dist/index.html"));
export default app;
- Add the script to run the app to the
package.json
{
"scripts": {
"dev": "bun run --hot index.ts"
}
}
Basics
Basic Routing
Create a new routing instance by instantiating the Hono
class. The top level hono instance of your server is usually called app
, and all hono instances have these routing methods:
app.get(route, (c) => {})
: GET routeapp.post(route, (c) => {})
: POST routeapp.put(route, (c) => {})
: PUT routeapp.delete(route, (c) => {})
: DELETE routeapp.all(route, (c) => {})
: route that catches all requests to that route, no matter which HTTP verb
You should also export default the top level file/routing instance you have, or some other object - but we'll get to that later.
You can even catch multiple methods or paths.
// Multiple Method
app.on(["PUT", "DELETE"], "/post", (c) => c.text("PUT or DELETE /post"));
// Multiple Paths
app.on("GET", ["/hello", "/ja/hello", "/en/hello"], (c) => c.text("Hello"));
Path parameters
Use :
before some text to make that text a path parameter variable.
app.get("/user/:name", (c) => {
const name = c.req.param("name");
//...
});
app.get("/posts/:id/comment/:comment_id", (c) => {
const { id, comment_id } = c.req.param();
//...
});
You can even make your path parameters optional by suffixing the path parameter variable name with a ?
.
// Will match `/api/animal` and `/api/animal/:type`
app.get("/api/animal/:type?", (c) => c.text("Animal!"));
Context object
The c
object is the context object, and you can get access to the request and response of the server route through c.request
and c.response
respectively, which are both web standard Request
and Response
classes from browser JavaScript.
c.header(key: string, value: string)
: sets a response header.c.status(code: number)
: sets the response status codec.text(text: string)
: returns a plaintext responsec.json(obj)
: returns a json responsec.html(content: string)
: returns an HTML response, which is a text/html responsec.notFound()
: returns a not found 404 responsec.redirect(route)
: redirects to the specified routec.error
: any error that a middleware handler throws
Instead of returning something using the context object, you can also return a web standard Response
instance, like so:
app.get("/", (c) => {
return new Response("Thank you for coming", {
status: 201,
headers: {
"X-Message": "Hello!",
"Content-Type": "text/plain",
},
});
});
c.req
The c.req
property is a HonoRequest
instance, which is a wrapper around the Request
web standard class.
getting info
c.req.param(routeParamName)
: gets the value of the specified route parameterc.req.param()
: returns all the route parameters as an object (key-value pairs).c.req.query(queryParamName)
: gets the value of the specified query parameterc.req.query()
: returns all the query parameters as an object (key-value pairs).c.req.header(headerKey)
: returns the value of the specified request headerc.req.routePath
: returns the route string of the request, like/people/:name
c.req.path
: returns the path of the request, like/people/aadil
c.req.url
: returns the full URL of the requestc.req.raw
: returns theRequest
instance that makes up this request
body parsing
c.req.parseBody()
: an async method that parses the request body and returns it if request content type ismultipart/formdata
orapplication/x-www-urlencoded
.c.req.text()
: as async method that parses the request body and returns it for plaintext requestsc.req.json()
: as async method that parses the request body and returns it for JSON requestsc.req.arrayBuffer()
: as async method that parses the request body and returns it as an array bufferc.req.blob()
: as async method that parses the request body and returns it as a blob example
app.get("/", (c) => {
const userAgent = c.req.header("User-Agent");
const body = await c.req.parseBody();
return c.json({ path: c.req.routePath });
});
Passing around data
You can pass around data like locals in express with the c.set(key, value)
and the c.get(key)
methods, but this requires middleware since these values expire when the request chain ends with a server response.
app.use(async (c, next) => {
// set local
c.set("message", "Hono is cool!!");
// go to next middleware in middleware chain
await next();
});
app.get("/", (c) => {
// get local
const message = c.get("message");
return c.text(`The message is "${message}"`);
});
And you can add type safety like so:
type Variables = {
message: string;
};
const app = new Hono<{ Variables: Variables }>();
Routers
You have the routers concept as you have in express.
- Create a router by creating a new hono instance and export default that.
import { Hono } from "hono";
const apiRouter = new Hono();
apiRouter.get("/ping", (c) => {
return c.json({ message: "pinged api successfully" });
});
export default apiRouter;
- Use the
app.route(route, router)
method to connect a router to a route prefix.
import { Hono } from "hono";
import apiRouter from "./routes/apiRouter";
const app = new Hono();
app.route("/api", apiRouter); // prefix with /api
Other routing
404 pages
Here is how you can customize when a user gets a 404 error
app.notFound((c) => {
return c.text("Custom 404 Message", 404);
});
Error handling
You can throw HTTPException
instances, which will go to a custom hono error handler.
Here is how you instantiate an HTTPException
:
throw new HTTPException(code, options);
code
: the status code to throw, like 400.options
: an object of options, with these keys:message
: a string error message to sendres
: aResponse
instance that carries data about the error
import { HTTPException } from "hono/http-exception";
app.post("/auth", async (c, next) => {
// authentication
if (authorized === false) {
throw new HTTPException(401, { message: "Custom error message" });
}
await next();
});
const errorResponse = new Response("Unauthorized", {
status: 401,
headers: {
Authenticate: 'error="invalid_token"',
},
});
throw new HTTPException(401, { res: errorResponse });
Once you throw an exception, you can catch them by registering a hono error handler. This is how you can set up a basic error handler:
app.onError((err, c) => {
console.error(`${err}`);
if (err instanceof HTTPException) {
// Get the custom response
return err.getResponse();
}
return c.text("Custom Error Message", 500);
});
Middleware
Using the hono instance app.use()
method, you can use middleware. This is how you create middleware:
app.use(async (c, next) => {
// ...
await next(); // go to next in request chain
});
For longer middleware we want to put into separate files, we can use the createMiddleware()
helper which gives us type intellisense for the c
and next
parameters, and then import that into the app.use()
.
import { createMiddleware } from "hono/factory";
const logger = createMiddleware(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`);
await next();
});
Here is an example of how to use middleware, where route handlers have a second argument after c
, which is the async next()
function.
app.use(async (_, next) => {
console.log("middleware 1 start");
await next();
console.log("middleware 1 end");
});
app.use(async (_, next) => {
console.log("middleware 2 start");
await next();
console.log("middleware 2 end");
});
app.use(async (_, next) => {
console.log("middleware 3 start");
await next();
console.log("middleware 3 end");
});
app.get("/", (c) => {
console.log("handler");
return c.text("Hello!");
});
Zod Validator Middleware
Validators allow you to validate request bodies coming into routes.
npm i zod
npm i @hono/zod-validator
import { zValidator } from '@hono/zod-validator'
- Use the
zValidator(type, schema)
function that returns middleware to validate request bodies. Returns error message if not valid
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
// 1. create schema
const schema = z.object({
name: z.string(),
age: z.number(),
});
// 2. apply middleware for json body
app.post("/author", zValidator("json", schema), (c) => {
// 3. get data back if valid
const data = c.req.valid("json");
return c.json({
success: true,
message: `${data.name} is ${data.age}`,
});
});
The zValidator(type, schema)
function returns a validation middleware you can apply before returning something in a route. Here is how to use it:
type
: The request body type you're validating. Here are the different types:"json"
: for validating json"form"
: for validating form data (multipart and url encoded)"query"
: for validating query parameters"header"
: for validating headers"param"
: for validating route parameters"cookie"
: for validating cookies
schema
: A Zod schema to validate against, created using thez
object.
You can even hook into the error response and provide a custom error response if the validation fails or succeeds.
app.post(
"/post",
zValidator("json", schema, (result, c) => {
if (!result.success) {
return c.text("Invalid!", 400);
}
})
//...
);
Export default: changing the port
In cases where you want to change the port you listen on (by default it's 3000), then you can change the export default from the hono app instance to an object:
// instead of export default app
export default {
port: 3001, // required
fetch: app.fetch, // required
maxRequestBodySize: 1024 * 1024 * 200,
};
Helpers
JSX
- Change the compiler options in the
tsconfig.json
:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
- Import the
FC
type from"hono/jsx"
and rename any ts files you have to tsx files. Type JSX components with theFC
type, and render JSX with thec.html()
method.
import { Hono } from "hono";
import type { FC, PropsWithChildren } from "hono/jsx";
const apiRouter = new Hono();
// type as props with children
const Layout: FC = ({ children }: PropsWithChildren) => {
return (
<html>
<head>
<title>wow, what an API</title>
</head>
<body
style={{
backgroundColor: "lightblue",
}}
>
{children}
</body>
</html>
);
};
// type a prop parameter
const NamePlate: FC<{ name: string }> = ({ name }) => {
return (
<div>
<h1>Hi, my name is {name}</h1>
</div>
);
};
// return jsx with c.html()
apiRouter.get("/", (c) => {
return c.html(
<Layout>
<NamePlate name="Aadil mallick" />
</Layout>
);
});
Props with children
import { PropsWithChildren } from "hono/jsx";
type Post = {
id: number;
title: string;
};
function Component({ title, children }: PropsWithChildren<Post>) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
}
Context
You can use context pretty easily to avoid passing own props.
- Import the methods
import { createContext, useContext } from "hono/jsx";
- Create the context with
createContext()
. Pass in an object
const context = createContext(obj);
- Use the context in a component with
useContext()
import type { FC, PropsWithChildren } from "hono/jsx";
import { createContext, useContext } from "hono/jsx";
const apiRouter = new Hono();
// 1. create context
const PersonContext = createContext({
name: "Aadil",
age: 20,
});
const Intro: FC = () => {
const person = useContext(PersonContext);
return (
<div>
<h1>Hi, my name is {person.name}</h1>
<h2>I am {person.age} years old</h2>
</div>
);
};
apiRouter.get("/", (c) => {
return c.html(<Intro />);
});
Client components
You can also create client components using Hono, which allows you to use the DOM.
import { useState } from "hono/jsx";
import { render } from "hono/jsx/dom";
// 1. create component with state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
function App() {
return (
<html>
<body>
<Counter />
</body>
</html>
);
}
// render react component in a container
const root = document.getElementById("root");
render(<App />, root);
Testing in hono
Here are the things you need to keep in mind when testing routes in hono:
- import your
app
- Use the
app.request()
method to request routes
import { expect, test, describe } from "bun:test";
import app from "..";
describe("API routes", () => {
// test going to /api route
test("GET /api", async () => {
const res = await app.request("/api");
expect(res.status).toBe(200);
});
// test getting data from /api/ping route
test("GET /api/ping", async () => {
const res = await app.request("/api/ping");
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({ message: "pinged api successfully" });
});
});
Miscellaneous
Downloading files
This is how when a user requests this route, you send them a file download:
apiRouter.get("/README", (c) => {
// first arg: blob data of file to send
// second arg: options, headers
return new Response(Bun.file("README.md"), {
headers: {
"Content-Type": "text/markdown",
"Content-Disposition": "attachment; filename=README.md",
},
});
});
The two important headers to send that make a file downloadable:
"Content-Type"
"Content-Disposition"