Welcome to this comprehensive guide on Hono.js, a modern, lightweight, and incredibly fast web framework designed for the multi-runtime JavaScript ecosystem. Whether you’re aiming to build high-performance APIs, serverless functions, or full-stack applications at the edge, Hono.js provides a robust and delightful development experience. This document is crafted for absolute beginners, guiding you from the very basics to more advanced concepts, complete with practical examples and exercises.
1. Introduction to Hono.js (Latest version)
Hono, meaning “flame” (炎) in Japanese, is a fitting name for a framework that aims to bring speed and efficiency to your web development. It’s a next-generation web framework built on Web Standards, offering unparalleled flexibility and performance across various JavaScript runtimes. As of August 2025, Hono is actively developed and has reached a stable v4.9.2 release, with ongoing community contributions and a growing adoption in production environments.
What is Hono.js?
Hono.js is a small, simple, and ultrafast web framework. Unlike some monolithic frameworks, Hono embraces the Web Standards API (like Request and Response objects), making your code highly portable. This means the same Hono application can run seamlessly on:
- Cloudflare Workers: For serverless functions deployed globally at the edge.
- Fastly Compute: Another edge computing platform.
- Deno: A secure JavaScript and TypeScript runtime.
- Bun: An incredibly fast all-in-one JavaScript runtime.
- Vercel/Netlify Functions: Serverless platforms for easy deployment.
- AWS Lambda/Lambda@Edge: Amazon’s serverless compute service.
- Node.js: The traditional JavaScript runtime for backend development.
Hono distinguishes itself with:
- Ultrafast Performance: Its routing engine (
RegExpRouter) is designed for extreme speed, avoiding linear loops found in many other frameworks. - Lightweight Footprint: The core
hono/tinypreset is under 14KB minified, with zero external dependencies, making it ideal for edge environments where bundle size matters. For comparison, Express.js can be significantly larger. - Multi-runtime Compatibility: Write once, deploy anywhere across the modern JavaScript ecosystem.
- Batteries Included (and Optional): Hono provides a rich set of built-in and third-party middleware for common tasks like authentication (Basic, Bearer, JWT, Firebase), CORS, logging, caching, and more. You only bundle what you use.
- Delightful Developer Experience (DX): Hono boasts clean APIs and first-class TypeScript support, including powerful type inference for path parameters and request validation.
Why learn Hono.js?
Learning Hono.js in 2025 offers several compelling advantages:
- Speed and Efficiency: In an era where milliseconds matter, Hono’s performance is a game-changer, especially for API-driven applications and edge computing. It can significantly reduce latency and improve user experience.
- Modern JavaScript Ecosystem: Hono is built with modern JavaScript runtimes and Web Standards in mind, making it a future-proof skill. It’s perfectly positioned for serverless architectures and distributed systems.
- Versatility: Whether you’re building a simple API, a proxy, a microservice, or even a full-stack application (especially with HonoX), Hono’s flexibility allows you to tackle a wide range of projects.
- Developer Productivity: With its clean API, strong TypeScript support, and a growing collection of middleware and helpers, Hono allows you to “Write Less, do more.” Type inference and features like RPC (Remote Procedure Call) make development safer and faster.
- Industry Relevance: Companies like cdnjs, Cloudflare D1, Unkey, and OpenStatus are already using Hono in production, demonstrating its capability and reliability for real-world applications. As the landscape shifts towards edge computing, Hono’s adoption is likely to grow further.
- Lower Operational Overhead: When combined with serverless platforms like Cloudflare Workers, Google Cloud Run, or AWS Lambda, Hono minimizes the infrastructure you need to manage, leading to reduced operational costs and complexity.
A brief history
While relatively new compared to veterans like Express.js, Hono has rapidly gained traction since its inception. It emerged as a solution for building high-performance web applications that could leverage the growing popularity of edge runtimes. Its focus on Web Standards from the beginning has allowed it to adapt quickly to new runtimes like Bun and Deno, cementing its place as a versatile and modern choice. The project maintains an active GitHub repository, regularly pushing updates and features, with a supportive community.
Setting up your development environment
Before we dive into Hono, let’s set up your development environment. You’ll need Node.js (which includes npm) and optionally a modern runtime like Bun or Deno if you plan to experiment with them.
Prerequisites:
Node.js and npm:
- Download and install Node.js from the official website. This will also install npm (Node Package Manager).
- Verify your installation by running:You should see version numbers for both.
node -v npm -v
Text Editor/IDE:
- A code editor with good TypeScript support is highly recommended. Visual Studio Code (VS Code) is a popular choice.
Step-by-step instructions for a basic Hono project:
Hono provides a convenient command-line tool to quickly scaffold new projects.
Create a new Hono project: Open your terminal or command prompt and run the following command:
npm create hono@latest my-hono-appYou’ll be prompted to select a template. For general purposes, you can choose
nodejs(for Node.js environment) orcloudflare-workers(for Cloudflare Workers deployment), orbunif you have Bun installed. For this guide, let’s selectnodejs.? Which template do you want to use? (Use arrow keys) > nodejs cloudflare-workers bun deno ...After selecting, the command will create a new directory
my-hono-appwith a basic Hono project structure and install the necessary dependencies.Navigate into your project directory:
cd my-hono-appRun your Hono application: The generated project includes a
devscript to start your application with hot-reloading.npm run devYou should see output similar to:
Server is running on http://localhost:3000Access your application: Open your web browser and navigate to
http://localhost:3000. You should see the message “Welcome to the Cars API!” (if you chose the Node.js template for a CRUD API example) or “Hello Hono!” (for simpler templates).
Congratulations! You’ve successfully set up your Hono development environment and run your first Hono application.
2. Core Concepts and Fundamentals
In this section, we’ll break down the fundamental building blocks of Hono.js. Understanding these core concepts is crucial for building any application with Hono.
The Hono Instance (Hono)
At the heart of every Hono application is the Hono instance. This is where you define your routes, apply middleware, and configure your application’s behavior.
Detailed Explanation:
The Hono class is imported from the hono package. When you create an instance of Hono, you get an app object that acts as your main application entry point. This app object provides methods for defining HTTP routes (like app.get(), app.post()), applying middleware (app.use()), and handling various request and response operations.
Code Example:
Let’s start with the classic “Hello World!” example.
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; // For Node.js environment
// Create a new Hono application instance
const app = new Hono();
// Define a GET route for the root path ('/')
app.get('/', (c) => {
// 'c' is the Context object, which we'll discuss next
// c.text() sends a plain text response
return c.text('Hello Hono!');
});
// For Node.js, we need to explicitly serve the Hono app
// The port is optional, defaults to 3000
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
// export default app; // This export is for other runtimes like Cloudflare Workers
Explanation:
import { Hono } from 'hono';: Imports theHonoclass.const app = new Hono();: Creates a new Hono application instance.app.get('/', (c) => { ... });: This defines a route that responds toGETrequests on the root path/. The second argument is a handler function that receives aContextobject (c).return c.text('Hello Hono!');: Inside the handler, we usec.text()to send a plain text response back to the client.serve({ fetch: app.fetch, port: 3000 }, (info) => { ... });: For Node.js, we use@hono/node-server’sservefunction to start the HTTP server.app.fetchis Hono’s universalfetchhandler, compatible across runtimes.
Exercises/Mini-Challenges:
- Change the greeting: Modify the
c.text()message to say “Welcome to Hono, [Your Name]!”. - Add a new route: Create a new GET route at
/aboutthat returns a simple text message like “This is the about page.” - Experiment with different ports: Change the
portin theservefunction to4000and verify that your application runs on the new port.
The Context Object (c)
The Context object (c) is the central piece of data within any Hono handler. It provides access to the incoming request, allows you to construct and send responses, and enables sharing data between middleware and handlers.
Detailed Explanation:
Every Hono handler function receives the Context object as its first argument. It’s a powerful and versatile object that encapsulates:
- Request Information (
c.req): Access to HTTP methods, URL, headers, query parameters, path parameters, and the request body. Hono’sRequestobject largely adheres to the Web Fetch API’sRequestinterface, making it familiar for those with browser-side experience. - Response Generation: Methods to send various types of responses (text, JSON, HTML, redirect, etc.), set status codes, and headers.
- Contextual Data (
c.set(),c.get(),c.var): A way to store and retrieve arbitrary data that can be passed down the middleware chain to subsequent handlers. This is particularly useful for things like authenticated user data or database connections. - Environment Variables (
c.env): Access to environment variables, especially critical in edge runtimes like Cloudflare Workers.
Code Examples:
Accessing Request Information:
// src/index.ts (continuing from previous example)
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
// Accessing path parameters
app.get('/users/:id', (c) => {
const userId = c.req.param('id'); // Get 'id' from the path
return c.text(`Fetching user with ID: ${userId}`);
});
// Accessing query parameters
app.get('/search', (c) => {
const query = c.req.query('q'); // Get 'q' from query string like /search?q=hono
return c.text(`Searching for: ${query || 'nothing'}`);
});
// Accessing headers
app.get('/headers', (c) => {
const userAgent = c.req.header('User-Agent');
return c.text(`Your User-Agent is: ${userAgent}`);
});
// Handling JSON request body
app.post('/submit', async (c) => {
const data = await c.req.json(); // Parse JSON body
console.log('Received data:', data);
return c.json({ message: 'Data received successfully!', yourData: data }, 200); // Send JSON response with 200 OK
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Explanation:
c.req.param('id'): Retrieves the value of the dynamic segment:idfrom the URL.c.req.query('q'): Retrieves the value of theqquery parameter.c.req.header('User-Agent'): Retrieves the value of a specific HTTP header.await c.req.json(): Asynchronously parses the request body as JSON. Note theawaitkeyword, as this is an asynchronous operation.c.json(...): Sends a JSON response. The second argument can be the HTTP status code.
Setting and Getting Contextual Data:
// src/index.ts (continuing)
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.use('*', async (c, next) => {
// Set a value on the context that can be accessed later
c.set('appName', 'My Hono App');
console.log('Middleware: App name set');
await next(); // Pass control to the next middleware or handler
});
app.get('/info', (c) => {
const appName = c.get('appName'); // Retrieve the value
return c.text(`Welcome to ${appName}!`);
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Explanation:
app.use('*', async (c, next) => { ... });: This is an example of middleware. The*means it applies to all routes.next()is a function that passes control to the next middleware or the route handler.c.set('appName', 'My Hono App');: Stores a value'My Hono App'under the key'appName'on the context.c.get('appName');: Retrieves the value associated with the key'appName'.
Exercises/Mini-Challenges:
- Multiple Path Parameters: Create a route
/products/:category/:productIdand return a message that displays bothcategoryandproductId. - Combine Query and Path: Create a route
/articles/:idthat also accepts an optional query parameterauthor. Ifauthoris provided, include it in the response. - Read a Custom Header: Send a request with a custom header (e.g.,
X-Custom-Header: MyValue) and have your Hono app read and return its value. You can use tools like Postman orcurlfor this. - Process a Form Data (POST request): Create a
POSTroute/registerthat expectsapplication/x-www-form-urlencodedormultipart/form-data. Useawait c.req.form()to parse the data and return it as JSON.
Routing
Hono’s routing system is designed to be powerful and efficient, allowing you to define different behaviors for various HTTP methods and URL paths.
Detailed Explanation:
Hono provides methods for all standard HTTP verbs (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD). You can define static routes, routes with dynamic parameters, and even regular expression-based routes. Hono also supports hierarchical routing, which helps organize larger applications.
Code Examples:
Static Routes:
// src/routes/static.ts
import { Hono } from 'hono';
const staticApp = new Hono();
staticApp.get('/hello', (c) => c.text('Hello there!'));
staticApp.post('/data', (c) => c.text('Received POST request.'));
export default staticApp;
Routes with Path Parameters:
// src/routes/params.ts
import { Hono } from 'hono';
const paramApp = new Hono();
// Dynamic ID
paramApp.get('/items/:id', (c) => {
const itemId = c.req.param('id');
return c.text(`You requested item ID: ${itemId}`);
});
// Multiple dynamic segments
paramApp.get('/books/:genre/:title', (c) => {
const genre = c.req.param('genre');
const title = c.req.param('title');
return c.text(`You're looking for the book "${title}" in the "${genre}" genre.`);
});
export default paramApp;
Hierarchical Routing (app.route()):
This is a best practice for organizing larger applications. You can create separate Hono instances for different parts of your API and then “mount” them onto specific paths in your main application.
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import staticRoutes from './routes/static'; // Import your route modules
import paramRoutes from './routes/params';
import apiV1 from './routes/api/v1'; // Assuming a nested structure
const app = new Hono();
// Mount the imported Hono instances onto base paths
app.route('/static', staticRoutes); // All routes in staticRoutes will be prefixed with /static
app.route('/dynamic', paramRoutes); // All routes in paramRoutes will be prefixed with /dynamic
app.route('/api/v1', apiV1); // Nested prefixing
app.get('/', (c) => c.text('Welcome to the Main Hono App!'));
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
// Example of a nested Hono instance (e.g., src/routes/api/v1.ts)
// src/routes/api/v1.ts
import { Hono } from 'hono';
const v1Api = new Hono();
v1Api.get('/users', (c) => c.json([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]));
v1Api.get('/products', (c) => c.json([{ id: 101, name: 'Laptop' }, { id: 102, name: 'Mouse' }]));
export default v1Api;
Explanation:
app.route('/prefix', anotherHonoInstance): This method allows you to modularize your routes. All routes defined withinanotherHonoInstancewill automatically have/prefixadded to their path. This keeps your mainindex.tsclean and promotes better organization.
Exercises/Mini-Challenges:
- Create a Blog Module: Create a new file
src/routes/blog.ts. Define routes like/(for all posts) and/:slug(for a single post) withinblog.ts. Then, import and mount thisblogApponto/blogin your mainindex.ts. Test accessing/blogand/blog/my-first-post. - Combine Method Handling: In a single route handler, try to implement both a
GETand aPOSTmethod for the same path/data. Return different responses based on the HTTP method.- Hint: You can export
GETandPOSTfunctions from a module when usingcreateRoutefromhono/factory(introduced in Intermediate Topics) or create a newHonoinstance and chain methods.
- Hint: You can export
Middleware
Middleware functions are a powerful feature in Hono that allow you to execute code before or after your main route handlers. They are essential for tasks like logging, authentication, error handling, and modifying request/response objects.
Detailed Explanation:
Middleware functions in Hono are functions that take Context (c) and a next function as arguments.
c: The context object, as we’ve discussed, provides access to the request and allows response manipulation.next: A function that, when called, passes control to the next middleware in the chain or the final route handler. Ifnext()is not called, the request-response cycle typically terminates within that middleware.
Middleware can be applied globally to all routes (app.use('*', ...)) or to specific paths or groups of paths.
Code Examples:
Logger Middleware:
Hono provides a built-in logger middleware.
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { logger } from 'hono/logger'; // Import the logger middleware
const app = new Hono();
// Use the logger middleware globally
app.use(logger()); // This will log incoming requests
app.get('/', (c) => {
return c.text('Hello with Logging!');
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Custom Authentication Middleware:
// src/middlewares/auth.ts
import { MiddlewareHandler } from 'hono';
// A simple middleware to check for a specific header
const authMiddleware: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header('Authorization');
if (authHeader === 'Bearer my-secret-token') {
// If authenticated, you can set user data on the context
c.set('user', { id: '123', name: 'Authenticated User' });
await next(); // Proceed to the next middleware or handler
} else {
// If not authenticated, return an unauthorized response
return c.json({ message: 'Unauthorized' }, 401);
}
};
export default authMiddleware;
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import authMiddleware from './middlewares/auth'; // Import your custom middleware
const app = new Hono();
app.get('/', (c) => c.text('Public access.'));
// Apply authentication middleware only to routes under /protected
app.use('/protected/*', authMiddleware);
app.get('/protected/dashboard', (c) => {
// If we reach here, the user is authenticated
const user = c.get('user'); // Retrieve user data from context
return c.json({ message: `Welcome to the dashboard, ${user.name}!`, user });
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Explanation:
app.use(middlewareFunction): Applies a middleware function.app.use('/path/*', middlewareFunction): Applies middleware only to paths that match the pattern. The*acts as a wildcard.await next(): It’s crucial toawait next()if the subsequent middleware or handler might perform asynchronous operations.
Exercises/Mini-Challenges:
- Timing Middleware: Create a custom middleware that measures the time taken for a request to be processed. Log the elapsed time (e.g.,
Request to ${path} took ${time}ms).- Hint: You can record
Date.now()at the beginning of the middleware, callawait next(), and then recordDate.now()again to calculate the difference.
- Hint: You can record
- Rate Limiting: Research
hono-rate-limiter(as seen in the search results) and integrate it into your application to limit requests to a specific endpoint (e.g.,/login) to prevent brute-force attacks. - CORS Middleware: Integrate Hono’s built-in CORS middleware (
hono/cors) to allow cross-origin requests from a specific domain.
3. Intermediate Topics
Now that you have a solid understanding of Hono’s fundamentals, let’s explore some intermediate topics that will enhance your Hono.js applications.
Validation with Zod
Type-safe validation is a cornerstone of robust API development. Hono integrates seamlessly with popular schema validation libraries like Zod, allowing you to define expected data shapes and automatically validate incoming requests.
Detailed Explanation:
The @hono/zod-validator middleware is designed to work with Zod schemas. It allows you to validate json, form, query, param, header, and cookie data in a type-safe manner. If validation fails, the middleware can automatically return a 400 Bad Request error or allow you to customize the error handling.
Installation:
npm install zod @hono/zod-validator
# or
yarn add zod @hono/zod-validator
# or
pnpm add zod @hono/zod-validator
Code Example:
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { z } from 'zod'; // Import Zod
import { zValidator } from '@hono/zod-validator'; // Import Hono's Zod validator
const app = new Hono();
// Define a Zod schema for input validation
const userSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
email: z.string().email('Invalid email address'),
age: z.number().int().positive('Age must be a positive integer'),
});
type User = z.infer<typeof userSchema>; // Infer TypeScript type from Zod schema
app.post(
'/users',
zValidator('json', userSchema, (result, c) => {
// Custom error hook: This function runs if validation fails
if (!result.success) {
console.error('Validation failed:', result.error.issues);
return c.json({
message: 'Validation failed',
errors: result.error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message
}))
}, 400); // Return 400 Bad Request
}
}),
async (c) => {
// If we reach here, the request body is valid and type-safe!
const newUser: User = c.req.valid('json'); // Access the validated data
// In a real app, you'd save newUser to a database
console.log('Creating user:', newUser);
return c.json({ message: 'User created successfully', user: newUser }, 201);
}
);
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Explanation:
z.object(...): Defines an object schema with specified fields and their types.z.string().min(3, ...): Example of Zod’s chainable methods for validation rules.type User = z.infer<typeof userSchema>;: A powerful feature of Zod that allows you to derive TypeScript types directly from your schemas, ensuring type consistency.zValidator('json', userSchema, (result, c) => { ... }): This is the validation middleware.- The first argument
'json'specifies that we are validating the JSON request body. Other options include'form','query','param','header', and'cookie'. - The second argument
userSchemais the Zod schema to use for validation. - The third optional argument is a
hookfunction that gets executed if validation fails. This allows for custom error responses.
- The first argument
c.req.valid('json'): Once validated, you can access the type-safe validated data usingc.req.valid().
Exercises/Mini-Challenges:
- Query Parameter Validation: Create a GET route
/productsthat accepts optionallimit(number) andoffset(number) query parameters. Use Zod to validate these parameters, ensuring they are positive integers. - Path Parameter Validation: Create a GET route
/posts/:idwhereidis validated as a UUID using Zod’sz.string().uuid(). - Combine Validations: Create a
POSTroute/orderthat validates both the JSON request body (e.g.,items: string[], total: number) and aUser-Agentheader (e.g.,z.string().startsWith('Mozilla')). Provide appropriate error messages for each.
Handling Different Response Types
Hono makes it easy to send various types of responses beyond just plain text, including JSON, HTML (with JSX support), and redirects.
Detailed Explanation:
The Context object (c) provides several helper methods for crafting responses:
c.json(data, status?): Sends a JSON response.c.text(content, status?, headers?): Sends a plain text response.c.html(content, status?, headers?): Sends an HTML response. Hono has built-in support for JSX rendering when usinghono/jsx.c.redirect(url, status?): Redirects the client to a different URL.c.notFound(): Returns a 404 Not Found response.c.body(body, status?, headers?): A more general method to send any kind of body (e.g., a stream, a buffer).
Code Examples:
JSON Response:
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/api/users', (c) => {
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
return c.json(users); // Sends JSON with default 200 OK status
});
app.post('/api/create-item', (c) => {
// Simulate item creation
const newItem = { id: Date.now(), name: 'New Item' };
return c.json({ message: 'Item created', item: newItem }, 201); // Sends JSON with 201 Created status
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
HTML Response with JSX:
First, you need to set up JSX. Create an _renderer.tsx file in app/routes (or a similar structure depending on your project setup for Honox). For a standalone Hono app, you can simply import hono/jsx.
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { html } from 'hono/html'; // Import html helper for JSX
const app = new Hono();
// A simple JSX component
const MyPage = ({ title, message }: { title: string; message: string }) => (
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{message}</h1>
<p>This page was rendered using Hono and JSX!</p>
</body>
</html>
);
app.get('/html-page', (c) => {
return c.html(<MyPage title="Hono JSX Example" message="Hello from Hono HTML!" />);
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Redirects:
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/old-path', (c) => {
return c.redirect('/new-path', 301); // Permanent redirect (301 Moved Permanently)
});
app.get('/new-path', (c) => {
return c.text('You landed on the new path!');
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Exercises/Mini-Challenges:
- Error Page: Create a route
/error-testthat returns a custom HTML page with a 500 status code and an error message like “Something went wrong!”. - Dynamic HTML: Modify the JSX example to display a list of items passed dynamically from the handler.
- Conditional Redirect: Create a route
/login-statusthat redirects to/dashboardif a specific query parameter (e.g.,loggedIn=true) is present, otherwise returns a “Please log in” message.
Error Handling
Robust error handling is critical for any production application. Hono provides a flexible way to catch and respond to errors that occur within your handlers or middleware.
Detailed Explanation:
Hono allows you to define a global error handler using app.onError(). This function catches any errors thrown within your routes or middleware. You can also define specific 404 Not Found handlers using app.notFound().
Code Example:
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
// Global Error Handler
app.onError((err, c) => {
console.error(`${err}`); // Log the error for debugging
return c.text('Internal Server Error', 500); // Send a generic 500 response
});
// 404 Not Found Handler
app.notFound((c) => {
return c.text('Custom 404: Not Found', 404);
});
app.get('/', (c) => {
return c.text('Home Page');
});
app.get('/trigger-error', (c) => {
throw new Error('This is a simulated error!'); // This will be caught by app.onError
});
app.get('/users/:id', (c) => {
const userId = c.req.param('id');
if (userId === '0') {
throw new Error('User ID cannot be 0'); // Another error example
}
return c.text(`User ID: ${userId}`);
});
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
Explanation:
app.onError((err, c) => { ... }): This function is called whenever an error is thrown within a route handler or middleware. You receive the error object (err) and theContext(c). This is where you can log the error, send a user-friendly error message, or perform any cleanup.app.notFound((c) => { ... }): This handler is invoked when no route matches the incoming request URL. It allows you to customize the 404 response.
Exercises/Mini-Challenges:
- JSON Error Response: Modify the
app.onErrorhandler to return a JSON response with details about the error (but be careful not to expose sensitive information in production!). - Specific Error Page: Create a custom HTML page for 404 errors and use
c.html()inapp.notFoundto render it.
4. Advanced Topics and Best Practices
Having covered the core and intermediate aspects, let’s explore more advanced topics and delve into best practices that ensure your Hono.js applications are scalable, maintainable, and robust.
Hono Stacks and RPC
Hono’s RPC (Remote Procedure Call) feature, often used in “Hono Stacks,” provides a highly type-safe way to define your API on the server and generate a client that understands those types. This eliminates a common source of bugs in full-stack development.
Detailed Explanation:
The Hono Stack typically involves:
- Hono: For defining your API endpoints.
- Zod: For request and response validation, ensuring data integrity.
@hono/zod-validator: The middleware connecting Zod with Hono.hono/client(hc): A client library that can infer types from your server-side Hono application, enabling type-safe API calls from your frontend.
The magic happens when you export the typeof your Hono route or app. The hc client then uses this type information to provide auto-completion and compile-time checks for your API calls.
Code Example:
Server-Side (src/server.ts):
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const app = new Hono();
const userSchema = z.object({
id: z.string(),
name: z.string().min(2),
email: z.string().email(),
});
// Define a route and chain methods to ensure proper type inference for RPC
const userRoute = app
.get('/users/:id', zValidator('param', z.object({ id: z.string().uuid() })), (c) => {
const { id } = c.req.valid('param');
// In a real app, fetch user from DB
return c.json({ id, name: `User ${id}`, email: `${id}@example.com` });
})
.post('/users', zValidator('json', userSchema), async (c) => {
const newUser = c.req.valid('json');
return c.json({ message: 'User created', user: newUser }, 201);
});
// Export the type of the route for client-side type inference
export type AppType = typeof userRoute;
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server running for RPC at http://localhost:${info.port}`);
});
Client-Side (src/client.ts - e.g., in a React or Vue project):
import { hc } from 'hono/client';
import type { AppType } from './server'; // Import the server's type
// Create a client instance, passing the server's type
const client = hc<AppType>('http://localhost:3000');
async function fetchUser(id: string) {
try {
// client.users[':id'] for path parameters (note the string literal for ':id')
const res = await client.users[':id'].$get({
param: { id: id }, // Type-safe parameter
});
const data = await res.json();
console.log('Fetched user:', data);
} catch (error) {
console.error('Error fetching user:', error);
}
}
async function createUser() {
const newUser = {
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
name: 'Jane Doe',
email: 'jane@example.com',
};
try {
const res = await client.users.$post({
json: newUser, // Type-safe JSON body
});
const data = await res.json();
console.log('Created user:', data);
} catch (error: any) {
if (error.res) { // Hono's client sometimes wraps errors with a response object
const errorData = await error.res.json();
console.error('Validation error on create:', errorData);
} else {
console.error('General error creating user:', error);
}
}
}
// Call the functions (example usage)
fetchUser('f8d1c2a7-3e4b-4c5d-6f7e-890123456789'); // Valid UUID
fetchUser('invalid-uuid'); // This will trigger a server-side validation error
createUser();
createUser({ name: 'JD', email: 'invalid-email' }); // This will trigger server-side validation error
Explanation:
- Server-Side Chaining: For RPC to infer routes correctly, all included methods (like
.get,.post) should be chained, and the endpoint or app type must be inferred from a declared variable (typeof userRoute). This helps Hono’s type system understand the full API structure. hc<AppType>(baseURL): Thehcfunction creates an HTTP client. By passingAppTypeas a generic, it gains full type knowledge of your server API.- Type-Safe Calls: Notice how
client.users[':id'].$get({ param: { id: id } })provides type hints for both the path parameters and the method. Similarly,$post({ json: newUser })expects a JSON body matching theuserSchema. - Error Handling in Client: The client can catch errors from the server, and
error.res.json()can often retrieve the detailed error response, especially when using the custom validation error hook on the server.
Best Practices for RPC:
- Centralize Schemas: Define your Zod schemas in a shared
dtosorschemasdirectory that both your server and client can import. - Chain Routes: Always chain your route definitions (
app.get(...).post(...)) to ensure proper type inference for the RPC client. - Export App Type: Explicitly export the
typeofyour Hono app or specific route groups.
Building Larger Applications (Modularity)
For larger projects, keeping all routes and logic in a single file quickly becomes unmanageable. Hono encourages modularity through app.route() and organizing your code into features or domains.
Detailed Explanation:
As shown in the “Routing” section, app.route() is the primary mechanism for modularity. You create separate Hono instances for distinct API concerns (e.g., users, products, auth) and then mount them on your main application.
Best Practices:
- Feature-Based Directory Structure: Organize your code by features, not just by type (e.g.,
src/features/users,src/features/products)./src ├── features/ │ ├── auth/ │ │ ├── auth.routes.ts │ │ ├── auth.service.ts │ │ └── auth.schemas.ts │ ├── users/ │ │ ├── users.routes.ts │ │ ├── users.service.ts │ │ └── users.schemas.ts ├── middlewares/ │ ├── auth.middleware.ts │ └── logger.middleware.ts ├── utils/ │ └── common.utils.ts └── app.ts (main entry point) - Separate Route Definitions: Each feature should have its own
Honoinstance and route definitions. - Use
app.route(): Mount these feature-specific Hono instances in your mainapp.ts.
Example (revisiting from Routing):
// src/features/users/users.routes.ts
import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const usersApp = new Hono();
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
usersApp.get('/', (c) => {
// Logic to fetch all users
return c.json([{ id: 'u1', name: 'John Doe', email: 'john@example.com' }]);
});
usersApp.get('/:id', (c) => {
const { id } = c.req.param(); // No need for zValidator here if we assume ID format in param type
// Logic to fetch user by ID
return c.json({ id, name: `User ${id}`, email: `${id}@example.com` });
});
usersApp.post('/', zValidator('json', userSchema), async (c) => {
const newUser = c.req.valid('json');
// Logic to create user
return c.json({ message: 'User created', user: newUser }, 201);
});
export default usersApp;
// src/app.ts (main entry point)
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { logger } from 'hono/logger';
import usersRoutes from './features/users/users.routes'; // Import feature routes
const app = new Hono();
app.use(logger());
// Mount feature routes
app.route('/users', usersRoutes); // All routes in usersRoutes will be under /users
app.get('/', (c) => c.text('Welcome to the Main API!'));
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server running at http://localhost:${info.port}`);
});
Exercises/Mini-Challenges:
- Create a Product Feature: Implement a
src/features/productsdirectory withproducts.routes.ts,products.service.ts(a dummy service), andproducts.schemas.ts. Define routes forGET /products,GET /products/:id, andPOST /products. Mount thisproductsApponto/productsin your mainapp.ts. - Authentication per Feature: Apply your custom authentication middleware (from Core Concepts) only to the
usersfeature, ensuring that product-related routes remain public.
Deployment Considerations
Hono’s multi-runtime support means deployment strategies vary.
Detailed Explanation & Best Practices:
- Cloudflare Workers: This is a primary target for Hono due to its edge performance.
- Tooling: Use
wranglerCLI (npm i -g wrangler). - Entry Point: Your
index.ts(orsrc/index.ts) typically exports theHonoapp directly:export default app;orexport const onRequest = handle(app, '/api');if using Cloudflare Pages. - Deployment:
wrangler deploy
- Tooling: Use
- Bun: Hono works seamlessly with Bun.
- Setup: Use
bun create hono@latest --template bun. - Entry Point: Export an object with
portandfetch:export default { port: process.env.PORT ?? 3000, fetch: app.fetch }. - Deployment: Can be deployed as a traditional server or in containerized environments (Docker).
- Setup: Use
- Node.js: For traditional server environments.
- Setup: Use
npm create hono@latest --template nodejs. - Entry Point: Use
@hono/node-server’sservefunction as shown in earlier examples. - Deployment: Can be deployed to VPS, cloud VMs, Heroku, or containerized (Docker).
- Setup: Use
- Docker: For consistent deployment across various environments.
- Multi-stage builds: Use a multi-stage Dockerfile to keep the final image small.
- Production dependencies: Only copy production dependencies.
- Bun compiled binary: For Bun, you can even compile your Hono app into a standalone binary for extremely small and fast images.
Example Dockerfile (for a Bun-based Hono app):
# Build stage
FROM oven/bun:1.2-debian AS build
WORKDIR /app
# Copy dependencies
COPY bun.lock package.json ./
# Build dependencies
RUN bun install --frozen-lockfile --production --verbose
# Copy source and compile
COPY . .
# Add .dockerignore to exclude dev dependencies and other non-essential files
# Example .dockerignore:
# node_modules
# Dockerfile*
# .dockerignore
# .git
# .gitignore
# README.md
# LICENSE
# .vscode
# Makefile
# helm-charts
# .env
# .editorconfig
# .idea
# coverage*
RUN bun build --compile --minify --sourcemap ./src --outfile hono-docker-app
# Our application runner
FROM gcr.io/distroless/base-debian12:nonroot AS runner
ENV NODE_ENV=production
WORKDIR /app
# Add an unprivileged user for security
USER nonroot
ARG BUILD_APP_PORT=3000
ENV APP_PORT=${BUILD_APP_PORT}
EXPOSE ${APP_PORT}
# Copy the compiled executable from the build stage
COPY --from=build /app/hono-docker-app .
ENTRYPOINT ["./hono-docker-app"]
Tips for Cloud Deployment (General):
- Environment Variables: Always use environment variables for sensitive information (API keys, database credentials) and configuration.
- Logging: Ensure your application logs are accessible in your deployment environment (e.g., through stdout/stderr that cloud platforms capture).
- Monitoring: Integrate monitoring tools (like Sentry for Hono) to track performance and errors in production.
5. Guided Projects
These guided projects will help you apply the concepts you’ve learned to build practical Hono.js applications. Each project is broken down into manageable steps.
Project 1: Simple Todo API with Memory Storage
Objective: Build a basic RESTful API for managing todos, storing them in memory (no database needed for simplicity).
Concepts Applied:
- Hono instance
- Routing (
GET,POST,PUT,DELETE) - Context object (
c.json,c.req.json,c.req.param) - Middleware (optional logging)
- Basic data handling
Steps:
Initialize Project: If you haven’t already, create a new Hono project:
npm create hono@latest hono-todo-api cd hono-todo-apiChoose the
nodejstemplate.Define Todo Structure and In-Memory Storage: Open
src/index.ts. First, define a TypeScript interface for ourTodoitem and an array to store them.// src/index.ts import { Hono } from 'hono'; import { serve } from '@hono/node-server'; import { logger } from 'hono/logger'; // Optional: for logging requests // --- In-memory storage and types --- interface Todo { id: string; title: string; completed: boolean; } const todos: Todo[] = []; // Our "database" // --- Hono App Setup --- const app = new Hono(); app.use(logger()); // Use logger middleware // ... rest of the code will go hereCreate Todo (POST /todos): Add a route to allow users to create new todos. We’ll expect a
titlein the request body.// src/index.ts (add inside app setup) // ... app.post('/todos', async (c) => { const { title } = await c.req.json() as { title: string }; // Assuming title is sent in JSON body if (!title) { return c.json({ error: 'Title is required' }, 400); } const newTodo: Todo = { id: crypto.randomUUID(), // Generates a unique ID title, completed: false, }; todos.push(newTodo); return c.json(newTodo, 201); // 201 Created }); // ...- Self-challenge: Add Zod validation for the
titleto ensure it’s a non-empty string.
- Self-challenge: Add Zod validation for the
Get All Todos (GET /todos): Add a route to fetch all existing todos.
// src/index.ts (add inside app setup) // ... app.get('/todos', (c) => { return c.json(todos); // Returns all todos }); // ...Get Single Todo by ID (GET /todos/:id): Add a route to fetch a specific todo by its ID.
// src/index.ts (add inside app setup) // ... app.get('/todos/:id', (c) => { const { id } = c.req.param(); const todo = todos.find(t => t.id === id); if (!todo) { return c.json({ error: 'Todo not found' }, 404); } return c.json(todo); }); // ...Update Todo (PUT /todos/:id): Add a route to update an existing todo. We’ll allow updating
titleandcompletedstatus.// src/index.ts (add inside app setup) // ... app.put('/todos/:id', async (c) => { const { id } = c.req.param(); const body = await c.req.json() as Partial<Todo>; // Allow partial updates const todoIndex = todos.findIndex(t => t.id === id); if (todoIndex === -1) { return c.json({ error: 'Todo not found' }, 404); } // Update the todo todos[todoIndex] = { ...todos[todoIndex], ...body }; return c.json(todos[todoIndex]); }); // ...- Self-challenge: Use Zod validation for the update body to ensure
titleis a string andcompletedis a boolean.
- Self-challenge: Use Zod validation for the update body to ensure
Delete Todo (DELETE /todos/:id): Add a route to delete a todo by its ID.
// src/index.ts (add inside app setup) // ... app.delete('/todos/:id', (c) => { const { id } = c.req.param(); const initialLength = todos.length; const filteredTodos = todos.filter(t => t.id !== id); if (filteredTodos.length === initialLength) { return c.json({ error: 'Todo not found' }, 404); } // Update the in-memory array todos.splice(0, todos.length, ...filteredTodos); // Replace content with filtered array return c.json({ message: 'Todo deleted' }, 204); // 204 No Content for successful deletion }); // ...Complete the Server Setup: Ensure your
servecall is at the end ofsrc/index.ts.// src/index.ts (at the very end) // ... serve({ fetch: app.fetch, port: 3000 }, (info) => { console.log(`Server is running on http://localhost:${info.port}`); });Test Your API: Run
npm run devand use a tool like Postman, Insomnia, orcurlto test your API endpoints:POST http://localhost:3000/todoswith JSON body:{"title": "Learn Hono"}GET http://localhost:3000/todosGET http://localhost:3000/todos/:id(use an ID from the POST response)PUT http://localhost:3000/todos/:idwith JSON body:{"completed": true}DELETE http://localhost:3000/todos/:id
Project 2: Frontend-Integrated Full-Stack Hono App with HonoX
Objective: Build a simple full-stack application using Hono for the backend API and Honox for server-side rendered (SSR) pages with client-side interactivity (Islands architecture). This demonstrates Hono’s capability beyond just APIs.
Concepts Applied:
- Hono as an API backend
- HonoX for SSR and File-based routing
- JSX rendering
- Client-side hydration (Islands)
- Type-safe API calls with
hono/client(hc)
Steps:
Initialize Honox Project: Honox is Hono’s meta-framework for full-stack development. It sets up Vite for bundling.
npm create hono@latest honox-fullstack-app cd honox-fullstack-appWhen prompted, choose the
x-basictemplate (orx-islandsfor more complex client-side interaction examples).Explore the Initial Structure: After installation, you’ll see a structure similar to this:
. ├── app │ ├── global.d.ts // Global type definitions │ ├── routes │ │ ├── _renderer.tsx // Main JSX renderer for pages │ │ └── index.tsx // Root route for your application │ └── server.ts // Server entry file for Honox ├── package.json ├── tsconfig.json └── vite.config.tsUnderstand
app/server.ts: This file is the entry point for your HonoX server. It creates the main Hono app instance.// app/server.ts import { createApp } from 'honox/server'; import { showRoutes } from 'hono/dev'; // Useful for debugging routes const app = createApp(); // You can add global Hono middleware here // app.use(logger()); showRoutes(app); // Log all defined routes on startup export default app;Understand
app/routes/_renderer.tsx: This file defines your main HTML layout and the JSX renderer. All your page components will be rendered within this layout.// app/routes/_renderer.tsx import { jsxRenderer } from 'hono/jsx-renderer'; export default jsxRenderer(({ children, title }) => { return ( <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {title ? <title>{title}</title> : <title>My HonoX App</title>} {/* You can link CSS files here */} </head> <body> {children} </body> </html> ); });Create Your First Page (
app/routes/index.tsx): This is your home page.// app/routes/index.tsx import { createRoute } from 'honox/factory'; // This is a simple server-rendered page export default createRoute((c) => { return c.render( <main> <h1>Welcome to My HonoX App!</h1> <p>This is a server-rendered page.</p> <a href="/api/hello">Check out the API!</a> </main>, { title: 'Home Page' } // Pass props to the renderer for the title ); });Add a Simple API Endpoint: You can define API endpoints directly within your
routesdirectory, or create a separateapisubdirectory. Let’s createapp/routes/api/hello.ts.// app/routes/api/hello.ts import { Hono } from 'hono'; const api = new Hono(); api.get('/', (c) => { return c.json({ message: 'Hello from Hono API!' }); }); // Export the Hono instance directly for file-based routing export default api;Now, when you visit
http://localhost:3000/api/hello, you’ll see the JSON response.Run the Honox App:
npm run devNavigate to
http://localhost:3000to see your home page andhttp://localhost:3000/api/hellofor the API.Integrate Client-Side Interactivity (Hono Islands - Optional Advanced Step): This step shows how Honox allows you to selectively hydrate parts of your page for client-side JavaScript. This is for the
x-islandstemplate. If you chosex-basic, this setup might require more manual configuration for client-side bundling.Let’s assume you’re using the
x-islandstemplate or have set up client-side bundling withhonox/vite.Create a client-side component (
app/islands/Counter.tsx):// app/islands/Counter.tsx import { useState } from 'react'; // Or 'preact/hooks' if using Preact export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }Use the island component in a page (
app/routes/counter.tsx):// app/routes/counter.tsx import { createRoute } from 'honox/factory'; import Counter from '../islands/Counter'; // Import the island component export default createRoute((c) => { return c.render( <main> <h1>Interactive Counter</h1> <Counter /> {/* This component will be hydrated on the client */} </main>, { title: 'Counter Page' } ); });- Self-challenge: Use
hono/client(hc) in an island component to fetch data from your/api/helloendpoint and display it. This demonstrates type-safe client-server communication.
- Self-challenge: Use
This project demonstrates how Hono can power both your API and your server-rendered web pages, offering a complete solution for modern web applications.
6. Bonus Section: Further Learning and Resources
Congratulations on making it this far! You now have a solid foundation in Hono.js. The journey of learning never ends, and here are some excellent resources to continue your exploration:
Recommended Online Courses/Tutorials
While Hono is relatively new, many developers and educators are creating content. Look for:
- Official Hono Guides: The Hono documentation itself (linked below) is structured like a tutorial and is an excellent place to start and continue.
- YouTube Tutorials: Search for “Hono.js tutorial,” “Hono Cloudflare Workers,” “Hono Bun,” etc. Channels focusing on modern JavaScript frameworks and serverless often feature Hono.
- Blog Series: Many developers publish multi-part series on building applications with Hono.
Official Documentation
- Hono Official Documentation: The absolute best resource for up-to-date information, API references, guides, and examples.
- https://hono.dev/
- Hono GitHub Repository - Explore the source code, issues, and discussions.
Blogs and Articles
- Medium.com: Many articles compare Hono with other frameworks, deep dive into specific features like validation, or provide deployment guides. Search for “Hono.js” on Medium.
- Examples from our search:
- “Express vs Koa vs Fastify vs NestJS vs Hono: Choosing the Right Node.js Framework” by Shahid Islam
- “The Death of Express.js? Comparing Fastify, Hono, and Bun’s Native HTTP Server” by asierr.dev
- “Using Bun, Hono, and Docker to Deploy Lightweight APIs” by Stanley Mohr
- “Build a Blazing Fast API in Minutes with Hono and Cloud Run” by Karl Weinmeister
- Examples from our search:
- Dev.to: Another great platform for community-driven technical articles.
- Example from our search: “Hacking Hono: The Ins and Outs of Validation Middleware” by fiberplane
YouTube Channels
Look for channels focused on:
- Serverless Development: Many Hono use cases align with serverless patterns.
- TypeScript Development: Hono’s strong TypeScript integration makes it a good fit for channels that emphasize type-safe code.
- Cloudflare Workers, Bun, Deno: Channels that explore these runtimes will often feature Hono.
Community Forums/Groups
- Hono.js Discord Server: This is often the most active place for direct interaction with the Hono community and maintainers, asking questions, and getting real-time help. (Look for a link on the official Hono website).
- GitHub Discussions: The Hono.js GitHub repository has a “Discussions” section where you can find answers to common questions and engage in architectural discussions.
- Stack Overflow: Search for Hono-related questions. As its popularity grows, more answers will become available.
Next Steps/Advanced Topics
After mastering the content in this document, consider exploring these advanced topics:
- Database Integration: Connect Hono to various databases (PostgreSQL, MongoDB, SQLite, Cloudflare D1/KV, Firestore).
- Authentication & Authorization: Implement more complex authentication flows (OAuth, OpenID Connect) and role-based access control (RBAC).
- Testing Hono Applications: Learn how to write unit and integration tests for your Hono routes and middleware.
- GraphQL with Hono: Explore
hono-graphqlmiddleware for building GraphQL APIs. - WebSockets: Integrate WebSockets for real-time communication.
- Static Site Generation (SSG): Use Hono’s helpers for generating static assets.
- Advanced Middleware Development: Write more complex custom middleware and understand Hono’s middleware composition.
- Performance Optimization: Deep dive into Hono’s router options and other optimization techniques.
- Production Deployment Best Practices: Explore advanced deployment strategies, CI/CD pipelines, and observability tools.
Keep building, keep experimenting, and enjoy the speed and flexibility Hono.js brings to your web development!