Hono: Routing

Somebody who likes to code
Hono is a fast web framework built on Web Standards that provides a powerful routing system with excellent TypeScript support. Its routing system provides an intuitive API for building RESTful applications while maintaining exceptional performance. This article explores Hono's routing mechanisms, from the core application objects to request handling, ending with a practical implementation.
The Hono Application Object
The Hono class serves as the foundation of every Hono application. It encapsulates the application's routing table, middleware stack, and request handling logic.
import { Hono } from 'hono'
const app = new Hono()
The Hono constructor accepts a single optional parameter, including the following properties:
strict: Controls strict mode for path matching (distinguishes/pathfrom/path/). The default value istrue.router: Specifies which router to use. The default router isSmartRouter.getPath: Custom function to extract path from request.
The Hono class also has three generic type parameters:
E: Defines the shape of environment-specific types for our application. UsesBlankEnvby default. The environment type extends from theEnvtype and defines two properties:Bindings: Access platform-specific resources, with full type safety, via thec.envvariable.Variables: Share typed data between middleware and handlers viac.set()andc.get().
type MyEnv = {
Variables: {
foo: string
}
Bindings: {
flag: boolean
}
}
const app = new Hono<MyEnv>()
app.get('/', (c) => {
const foo = c.get('foo')
const flag = c.env.flag
...
})
S: Enables automatic type inference for API validation and type-safe client generation. UsesBlankSchemaby default. The schema type extends from theSchematype. When we add routes, Hono's type system automatically constructs the schema using theToSchematype. Therefore, defining a schema type is not common.
BasePath: Enforces type safety for base path routing. Uses/by default.
These generics parameters enable Hono's compile-time type-safety and automatic client generation.
Hono Routing System
Hono's routing system matches incoming HTTP requests to registered handlers based on the request method and URL path. The router supports static paths, dynamic parameters, and wildcard patterns.
Route Registration
In Hono, we can register routes using several methods that are implemented in the Hono class:
HTTP Method Handlers
The most common way is using HTTP method handlers (get, post, put, delete, options, patch, all).
app.get('/api', (c) => c.text('Hello'))
Custom Methods with on()
Register handlers for custom or multiple HTTP methods.
app.on(['GET', 'POST'], '/api', (c) => c.text('Hello'))
Middleware with use()
Register middleware that runs before route handlers.
app.use('/api', middleware)
Mounting Sub-applications with route()
Mount another Hono instance under a specific path.
const userApp = new Hono()
userApp.get('/users', (c) => c.text('Hello'))
// Mount under /api/v1
app.route('/api/v1', userApp)
Base Path with basePath()
Create a new Hono instance with a base path prefix.
const api = new Hono().basePath('/api')
api.get('/users', (c) => c.text('Hello'))
// Results in: /api/users
Route Matching
Routes are matched using the router's match() method. Hono supports:
Static routes:
/usersParameter routes:
/users/:idWildcard routes:
/api/*Optional parameters:
/posts/:id?Custom regex patterns:
/files/:name{.*}
Route Matching Priority
Hono evaluates routes in the order they are defined. More specific routes should be registered before generic ones.
Request Processing
When a request hits our application:
Path Extraction: Hono extracts the path from the request URL.
Route Matching: The router finds matching handlers based on method and path.
Context Creation: A
Contextobject is created with request data.Handler Execution: Our code runs with access to the
Contextobject.
The Context Object
The Context class is the central hub for handling individual requests, providing access to request data, response helpers, and variable storage.
Properties
req: Provides access to theHonoRequestobject containing request data.env: Provide access to the environment bindings properties.error: Error object if the handler threw an error.res: Provides access to the Response object.
Methods
- The
Contextclass provides type-safe response helpers:
app.get('/text', (c) => c.text('Hello', 200))
app.get('/json', (c) => c.json({ message: 'Hello' }, 201))
app.get('/html', (c) => c.html('<h1>Hello</h1>'))
app.get('/redirect', (c) => c.redirect('/target'))
status: Sets the response HTTP status code.header: Sets HTTP headers for the response.
Variable Storage
The Context class provides type-safe variable storage through set() and get() methods:
app.use('*', async (c, next) => {
c.set('startTime', Date.now())
await next()
const duration = Date.now() - c.get('startTime')
c.header('X-Duration', duration.toString())
})
We can also access the value of a variable with c.var.
The HonoRequest Object
The HonoRequest class wraps the raw Request object, providing convenient accessors for request data.
path: The pathname of the request (without query string).method: HTTP method of the request.url: The full request URL.raw: The rawRequestobject.
Parameter Access
// Single parameter
const id = c.req.param('id')
// All parameters as object
const { id, name } = c.req.param()
Query String Handling
// Single query value
const page = c.req.query('page')
// All queries as object
const { page, limit } = c.req.query()
// Multiple values for same key
const tags = c.req.queries('tags') // string[]
Body Parsing
// JSON body
const data = await c.req.json<T>()
// Text body
const text = await c.req.text()
// Form data (multipart or urlencoded)
const formData = await c.req.parseBody()
// Raw body methods
const arrayBuffer = await c.req.arrayBuffer()
const blob = await c.req.blob()
const formData = await c.req.formData()
Headers
// Single header
const userAgent = c.req.header('User-Agent')
// All headers as object
const headers = c.req.header()
Building an Application
We'll implement a complete API for tracking price changes. This demonstrates practical routing patterns and context usage. This application will evolve as we write new articles about Hono. The starting point will be the project setup we built in the post Hono: Setting up the development environment. Let’s start by instaling the following packages:
npm install uuid
npm install http-status-codes
Create the file features/stores/store.ts file with the following content:
export type Store = {
storeId: string;
name: string;
url: string;
};
export const stores: Store[] = [];
Storedefines the TypeScript type for store objects.storesis a module-level array serving as in-memory storage.
Create the file features/stores/add-stores.ts file with the following content:
import { Hono } from 'hono';
import { v7 } from 'uuid';
import { StatusCodes } from 'http-status-codes';
import { stores } from './store.js';
export const addRoute = new Hono().post('/', async c => {
const { name, url } = await c.req.json();
const store = { name, url, storeId: v7() };
stores.push(store);
return c.json(store, StatusCodes.CREATED);
});
The addRoute handler in add-store.ts accepts JSON payloads and creates new stores.
c.req.json()parses the request body as JSON.v7()from theuuidpackage generates a UUID v7 identifier.stores.push(store)adds the new store to the in-memorystoresarray.c.json(store, StatusCodes.CREATED)returns the created store with a201status code.
Create the file features/stores/get-store.ts file with the following content:
import { Hono } from 'hono';
import { stores } from './store.js';
import { StatusCodes } from 'http-status-codes';
export const getRoute = new Hono().get('/:storeId', async c => {
const storeId = c.req.param('storeId');
const store = stores.find(s => s.storeId === storeId);
if (!store) {
return c.json(
{ message: `Store ${storeId} not found` },
StatusCodes.NOT_FOUND
);
}
return c.json(store, StatusCodes.OK);
});
The getRoute handler in get-store.ts fetches stores by ID.
- Returns
StatusCodes.NOT_FOUND(404) when the store doesn't exist.
Create the file features/stores/list-stores.ts file with the following content:
import { Hono } from 'hono';
import { stores } from './store.js';
import { StatusCodes } from 'http-status-codes';
export const listRoute = new Hono().get('/', async c => {
const { pageNumber, pageSize, name } = c.req.query();
const pn = parseInt(pageNumber || '1', 10);
const pz = parseInt(pageSize || '10', 10);
let filteredStores = stores;
if (name) {
filteredStores = stores.filter(store =>
store.name.toLowerCase().includes(name.toLowerCase())
);
}
const totalCount = filteredStores.length;
const startIndex = (pn - 1) * pz;
const endIndex = startIndex + pz;
const page = filteredStores.slice(startIndex, endIndex);
return c.json(
{
items: page,
pageNumber: pn,
pageSize: pz,
totalPages: Math.ceil(totalCount / pz),
totalCount,
},
StatusCodes.OK
);
});
The listRoute handler in list-stores.ts supports pagination and filtering.
c.req.query()retrieves all query parameters as an object.The handler implements pagination with
pageNumberandpageSizeparameters.Case-insensitive filtering is applied when the
nameparameter is provided.Returns pagination metadata for client consumption.
Create the file features/stores/edit-store.ts file with the following content:
import { Hono } from 'hono';
import { stores } from './store.js';
import { StatusCodes } from 'http-status-codes';
export const editRoute = new Hono().put('/:storeId', async c => {
const storeId = c.req.param('storeId');
const { name, url } = await c.req.json();
const store = stores.find(s => s.storeId === storeId);
if (!store) {
return c.json(
{ message: `Store ${storeId} not found` },
StatusCodes.NOT_FOUND
);
}
store.name = name;
store.url = url;
return c.json(store, StatusCodes.OK);
});
Combines route parameters (
storeId) and request body (name,url).Mutates the existing
storeobject directly in thestoresarray.Returns
404if the store doesn't exist.
As we mentioned, Hono supports composing applications from smaller, feature-focused sub-applications. Create the file features/stores/index.ts file with the following content:
import { Hono } from 'hono';
import { listRoute } from './list-stores.js';
import { addRoute } from './add-store.js';
import { getRoute } from './get-store.js';
import { editRoute } from './edit-store.js';
export const storeRoute = new Hono()
.basePath('/stores')
.route('/', listRoute)
.route('/', addRoute)
.route('/', getRoute)
.route('/', editRoute);
Each feature (
list,add,get,edit) has its own Hono instance.route('/', subRoute)mounts multiple sub-routes at the same base path.Hono automatically merges routes based on HTTP methods and path patterns.
Create the app.ts file with the following content:
import { Hono } from 'hono';
import { storeRoute } from './features/stores/index.js';
export const app = new Hono({ strict: false });
app.route('/api', storeRoute);
app.get('/live', c =>
c.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: Date.now(),
})
);
export type App = typeof app;
new Hono({ strict: false })creates a Hono instance with non-strict routing, allowing trailing slashes in URLs to be ignored.app.route()mounts sub-applications at specific base paths, enabling modular route organization.app.get()registers a simple health check endpoint.
Update the index.ts file with the following content:
import { serve } from '@hono/node-server';
import { ENV } from '@/env.js';
import { app } from './app.js';
serve(
{
fetch: app.fetch,
port: ENV.PORT,
},
info => {
console.log(
`Server(${ENV.NODE_ENV}) is running on http://localhost:${info.port}`
);
}
);
@hono/node-serverprovides the Node.js adapter for Hono.app.fetchis Hono's standard request handler compatible with the Fetch API.
The API demonstrated in this article showcases Hono's core routing capabilities, including:
Route definition and parameter extraction
Request body parsing and response formatting
Modular route organization with sub-applications
You can find all the code here. Thanks, and happy coding.




