Hono.js has become one of the most exciting web frameworks in the JavaScript ecosystem. As of February 2026, it's the go-to choice for developers who want ultrafast APIs that run on any JavaScript runtime — Cloudflare Workers, Bun, Deno, Node.js, AWS Lambda, and more.
In this tutorial, you'll build a complete type-safe REST API with Hono.js from scratch, including input validation with Zod, proper error handling, and middleware.
Why Hono.js?
Hono (meaning "flame" 🔥 in Japanese) stands out for several reasons:
- Ultrafast — Its RegExpRouter uses a trie-based approach, not linear loops
-
Lightweight — The
hono/tinypreset is under 14kB with zero dependencies - Multi-runtime — Write once, deploy anywhere (Cloudflare, Bun, Deno, Node.js, AWS Lambda)
- Type-safe — First-class TypeScript support with inferred types across your entire request/response chain
-
Built on Web Standards — Uses the
RequestandResponseAPIs you already know
Prerequisites
- Node.js 22+ (or Bun 1.2+)
- Basic TypeScript knowledge
- A code editor with TypeScript support
Step 1: Scaffold Your Project
The fastest way to start a Hono project:
npm create hono@latest my-api
cd my-api
npm install
When prompted, select your target runtime. For this tutorial, we'll use Node.js, but the code works on any runtime.
Next, install Zod for request validation:
npm install zod @hono/zod-validator
Step 2: Define Your Data Model
Let's build a simple task management API. Create src/models.ts:
import { z } from 'zod';
export const TaskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
status: z.enum(['todo', 'in_progress', 'done']),
createdAt: z.string().datetime(),
});
export const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
});
export const UpdateTaskSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(1000).optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
});
export type Task = z.infer<typeof TaskSchema>;
export type CreateTask = z.infer<typeof CreateTaskSchema>;
export type UpdateTask = z.infer<typeof UpdateTaskSchema>;
Notice how Zod gives us both runtime validation and TypeScript types from a single source of truth. No duplication.
Step 3: Build the API Routes
Replace the contents of src/index.ts:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { prettyJSON } from 'hono/pretty-json';
import { zValidator } from '@hono/zod-validator';
import { randomUUID } from 'crypto';
import { CreateTaskSchema, UpdateTaskSchema, type Task } from './models';
const app = new Hono();
// Middleware
app.use('*', logger());
app.use('*', cors());
app.use('*', prettyJSON());
// In-memory store (swap for a database in production)
const tasks = new Map<string, Task>();
// GET /tasks — List all tasks
app.get('/tasks', (c) => {
const allTasks = Array.from(tasks.values());
return c.json({ tasks: allTasks, count: allTasks.length });
});
// GET /tasks/:id — Get a single task
app.get('/tasks/:id', (c) => {
const task = tasks.get(c.req.param('id'));
if (!task) {
return c.json({ error: 'Task not found' }, 404);
}
return c.json(task);
});
// POST /tasks — Create a new task
app.post(
'/tasks',
zValidator('json', CreateTaskSchema),
(c) => {
const body = c.req.valid('json');
const task: Task = {
id: randomUUID(),
title: body.title,
description: body.description,
status: 'todo',
createdAt: new Date().toISOString(),
};
tasks.set(task.id, task);
return c.json(task, 201);
}
);
// PATCH /tasks/:id — Update a task
app.patch(
'/tasks/:id',
zValidator('json', UpdateTaskSchema),
(c) => {
const id = c.req.param('id');
const existing = tasks.get(id);
if (!existing) {
return c.json({ error: 'Task not found' }, 404);
}
const updates = c.req.valid('json');
const updated: Task = { ...existing, ...updates };
tasks.set(id, updated);
return c.json(updated);
}
);
// DELETE /tasks/:id — Delete a task
app.delete('/tasks/:id', (c) => {
const id = c.req.param('id');
if (!tasks.delete(id)) {
return c.json({ error: 'Task not found' }, 404);
}
return c.json({ message: 'Task deleted' });
});
export default app;
The zValidator middleware automatically validates the request body against your Zod schema before your handler runs. If validation fails, it returns a 400 error with detailed messages. The validated data is fully typed in your handler — no casts needed.
Step 4: Add Custom Error Handling
Hono provides a clean way to handle errors globally. Add this before your routes:
import { HTTPException } from 'hono/http-exception';
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json(
{ error: err.message, status: err.status },
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{ error: 'Internal server error', status: 500 },
500
);
});
// 404 handler
app.notFound((c) => {
return c.json(
{ error: 'Not found', path: c.req.path },
404
);
});
Step 5: Add Authentication Middleware
Let's add a simple API key middleware to protect our routes:
import { createMiddleware } from 'hono/factory';
const authMiddleware = createMiddleware(async (c, next) => {
const apiKey = c.req.header('X-API-Key');
if (!apiKey || apiKey !== process.env.API_KEY) {
return c.json({ error: 'Unauthorized' }, 401);
}
await next();
});
// Apply to all task routes
app.use('/tasks/*', authMiddleware);
Using createMiddleware from hono/factory gives you full type inference within your middleware — the context object knows about upstream middleware and variables.
Step 6: Run and Test
Start the server:
npm run dev
Test your endpoints:
# Create a task
curl -X POST http://localhost:3000/tasks \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-secret-key' \
-d '{"title": "Learn Hono.js", "description": "Build a REST API"}'
# List all tasks
curl http://localhost:3000/tasks \
-H 'X-API-Key: your-secret-key'
# Try invalid input (validation will reject it)
curl -X POST http://localhost:3000/tasks \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-secret-key' \
-d '{"title": ""}'
The last request returns a 400 with details about why validation failed — title must be at least 1 character.
Step 7: Deploy Anywhere
Hono's multi-runtime support means you can deploy the same code to different platforms with minimal changes.
Deploy to Cloudflare Workers:
npx wrangler deploy
Deploy with Bun:
// src/index.ts — just change the export
export default {
port: 3000,
fetch: app.fetch,
};
Deploy to AWS Lambda:
import { handle } from 'hono/aws-lambda';
export const handler = handle(app);
Same business logic, different deployment target. That's the power of building on Web Standards.
Performance Tip: Use RPC Mode
Hono has a built-in RPC feature that lets your frontend call your API with full type safety — no code generation needed:
// Server: export the type
const route = app.post(
'/tasks',
zValidator('json', CreateTaskSchema),
(c) => {
// ... handler
return c.json(task, 201);
}
);
export type AppType = typeof route;
// Client: use hc (Hono Client)
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
const res = await client.tasks.$post({
json: { title: 'Type-safe!' },
});
// res is fully typed — no codegen, no OpenAPI spec needed
This gives you end-to-end type safety from your API schema all the way to your frontend code.
Wrapping Up
Hono.js gives you the speed of lightweight frameworks with the developer experience of full-featured ones. Its combination of Web Standards, multi-runtime support, and first-class TypeScript makes it an excellent choice for building APIs in 2026.
Key takeaways:
- Use Zod +
@hono/zod-validatorfor type-safe validation - Leverage built-in middleware (
cors,logger,prettyJSON) instead of installing separate packages - Use
hono/factoryfor typed custom middleware - Deploy the same code anywhere with minimal adapter changes
- Try RPC mode for end-to-end type safety with your frontend
If you're building APIs that need to be fast, portable, and type-safe, give Hono.js a try. You might not go back.
Building APIs and need reliable infrastructure? Check out 1xAPI for production-ready API services including email verification, IP geolocation, and more.
Top comments (0)