Hono: Routing

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

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 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 /path from /path/). The default value is true.
router: Specifies which router to use. The default router is SmartRouter.
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. Uses BlankEnv by default. The environment type extends from the Env type and defines two properties:
Bindings: Access platform-specific resources, with full type safety, via the c.env variable.
Variables: Share typed data between middleware and handlers via c.set() and c.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. Uses BlankSchema by default. The schema type extends from the Schema type. When we add routes, Hono's type system automatically constructs the schema using the ToSchema type. 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'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.
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
Routes are matched using the router's match() method. Hono supports:
Static routes: /users
Parameter routes: /users/:id
Wildcard routes: /api/*
Optional parameters: /posts/:id?
Custom regex patterns: /files/:name{.*}
Hono evaluates routes in the order they are defined. More specific routes should be registered before generic ones.
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 Context object is created with request data.
Handler Execution: Our code runs with access to the Context object.
The Context class is the central hub for handling individual requests, providing access to request data, response helpers, and variable storage.
req: Provides access to the HonoRequest object 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.
Context class 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.
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 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 raw Request object.
// Single parameter
const id = c.req.param('id')
// All parameters as object
const { id, name } = c.req.param()
// 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[]
// 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()
// Single header
const userAgent = c.req.header('User-Agent')
// All headers as object
const headers = c.req.header()
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[] = [];
Store defines the TypeScript type for store objects.
stores is 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 the uuid package generates a UUID v7 identifier.
stores.push(store) adds the new store to the in-memory stores array.
c.json(store, StatusCodes.CREATED) returns the created store with a 201 status 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.
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 pageNumber and pageSize parameters.
Case-insensitive filtering is applied when the name parameter 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 store object directly in the stores array.
Returns 404 if 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-server provides the Node.js adapter for Hono.
app.fetch is 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.