¿Qué es Hono?
Hono es un framework web pequeño y rápido construido sobre la Web Platform. En lugar de inventar sus propios objetos de solicitud y respuesta, utiliza las clases estándar Request y Response que proporcionan los navegadores, Cloudflare Workers, Deno y Bun. Sobre esa base, añade enrutamiento, middleware y un objeto de contexto, y nada más de lo que no solicites.
El nombre significa “llama” en japonés, y el proyecto apuesta por la velocidad: un núcleo diminuto, sin dependencias integradas de Node y un router diseñado para cold starts. Esa combinación ha convertido a Hono en la opción predeterminada para APIs en el edge, donde cada milisegundo de arranque se paga en cada solicitud. Debido a que el contrato del runtime es el estándar web, la misma aplicación también se ejecuta en Node, por lo que nunca quedas atrapado en un solo ecosistema.
Enrutamiento basado en estándares web
El enrutamiento resulta deliberadamente familiar. Un método y una ruta se mapean a un handler, los parámetros utilizan :name y los comodines utilizan *.
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("Hello"));
app.get("/posts", (c) => c.json([]));
app.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/posts", (c) => c.json({ created: true }, 201));
Los routers pueden dividirse en sub-apps y montarse, que es la forma en que los servicios más grandes se mantienen organizados:
import { Hono } from "hono";
const api = new Hono();
api.get("/users", listUsers);
api.get("/users/:id", getUser);
app.route("/api", api);
Cada handler devuelve un Response. Los helpers de contexto — c.json, c.text, c.html, c.redirect, c.body — construyen la respuesta correcta con headers y status, y siempre puedes devolver un new Response(...) puro cuando necesites control total.
El objeto context
El único argumento del handler es el context, convencionalmente llamado c. Este contiene la request, los helpers de respuesta y un lugar para almacenar valores para la request actual.
app.post("/posts", async (c) => {
const id = c.req.param("id"); // path parameter
const page = c.req.query("page"); // query string
const body = await c.req.json(); // parsed body
const token = c.req.header("authorization");
c.set("requestId", crypto.randomUUID()); // per-request store
return c.json({ id, page, body, token });
});
c.env expone los bindings del runtime: variables de entorno, namespaces de KV, bases de datos D1 y buckets de R2 en Workers. c.set y c.get comparten valores entre middleware y handlers, y c.var proporciona acceso tipado a ellos. Todo lo que necesitas para una request reside en un solo objeto, lo que hace que la composición de middleware sea sencilla.
Middleware
El middleware es una función asíncrona que recibe el contexto y next. Llama a await next() para continuar la cadena, o retorna prematuramente para interrumpir el flujo.
import { createMiddleware } from "hono/factory";
export const timing = createMiddleware(async (c, next) => {
const start = performance.now();
await next();
c.header("Server-Timing", `app;dur=${performance.now() - start}`);
});
app.use("*", timing);
El helper createMiddleware añade inferencia de tipos, y app.use acepta un patrón de ruta para que el middleware solo se ejecute donde sea necesario. El orden de registro es el orden de ejecución, exactamente igual que en Express.
Hono incluye un conjunto de middleware muy útiles en su núcleo:
corspara encabezados de origen cruzado.loggerpara el registro de solicitudes (logging).bearerAuthybasicAuthpara autenticación.cachepara el almacenamiento en caché de respuestas en el edge.etag,compressysecureHeaderspara higiene de HTTP.csrfpara protección contra falsificación de solicitudes entre sitios (CSRF).
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { bearerAuth } from "hono/bearer-auth";
app.use("*", logger());
app.use("/api/*", cors());
app.use("/admin/*", bearerAuth({ token: c.env.ADMIN_TOKEN }));
Debido a que son middleware estándar, se componen con los tuyos propios y con cualquier otra herramienta del ecosistema.
Validación y respuestas tipadas
Hono cuenta con validadores nativos para Zod y Valibot. Estos analizan un objetivo (json, query, param, form), rechazan las entradas inválidas con un 400 y proporcionan al handler un valor tipado.
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const CreatePost = z.object({
title: z.string().min(1),
body: z.string(),
});
app.post("/posts", zValidator("json", CreatePost), (c) => {
const post = c.req.valid("json");
return c.json({ id: crypto.randomUUID(), ...post }, 201);
});
El valor analizado está tipado, por lo que c.req.valid("json") tiene exactamente la forma del esquema. Puedes personalizar la respuesta de error mediante un hook, lo que te permite mantener los cuerpos de los errores consistentes con el resto de tu API.
RPC: tipos end-to-end sin codegen
El modo RPC es la característica estrella de Hono. Si exportas el tipo de tu app, el cliente puede inferir cada ruta y respuesta directamente desde él.
// server.ts
const route = app
.get("/api/users", (c) => c.json([{ id: "1", name: "Ada" }]))
.post(
"/api/users",
zValidator("json", CreateUser),
(c) => c.json({ id: crypto.randomUUID(), ...c.req.valid("json") }, 201),
);
export type AppType = typeof route;
// client.ts
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("https://api.example.com");
const res = await client.api.users.$post({
json: { name: "Ada", email: "[email protected]" },
});
if (res.ok) {
const user = await res.json(); // typed from the server handler
}
No hay ningún archivo de esquema que mantener sincronizado ni un cliente generado. Cambia una ruta en el servidor y el cliente fallará al compilar hasta que se actualice, lo que convierte el API drift en un error de build. Esto funciona mejor en un monorepo o en un paquete compartido donde ambas partes puedan importar el tipo.
Renderizado de HTML y JSX
Hono puede servir HTML directamente, ya sea como strings o mediante su propio runtime de JSX. El JSX está diseñado para el renderizado en el servidor: sin DOM virtual, sin hidratación, simplemente una salida de strings.
import { Hono } from "hono";
import { html } from "hono/html";
const app = new Hono();
app.get("/", (c) => {
return c.html(
html`<!doctype html>
<html>
<body><h1>Hello from ${c.req.query("name") ?? "the edge"}</h1></body>
</html>`,
);
});
Para una experiencia de plantillas más completa, hono/jsx proporciona componentes y layouts, y helpers como html se encargan del escapado. Es una opción ideal para páginas renderizadas en el servidor y para fragmentos de HTML devueltos por una API en el edge.
Un solo codebase, múltiples runtimes
La portabilidad es la clave. El código de la aplicación nunca importa un módulo específico de un runtime; solo cambia el punto de entrada.
// Node
import { serve } from "@hono/node-server";
import app from "./app";
serve({ fetch: app.fetch, port: 3000 });
// Cloudflare Workers
export default app;
// Bun
export default { port: 3000, fetch: app.fetch };
El mismo objeto app sirve para los tres. Esto significa que un servicio prototipado en Node puede migrar a Workers por razones de costo o latencia sin necesidad de reescribirlo, y una aplicación de Workers puede ejecutarse en una suite de pruebas local a través del adaptador de Node.
Despliegue en el edge
En Cloudflare Workers, el despliegue se realiza mediante un comando de Wrangler y un pequeño archivo de configuración.
pnpm add -D wrangler
npx wrangler deploy
# wrangler.toml
name = "api"
main = "src/index.ts"
compatibility_date = "2026-09-01"
[[kv_namespaces]]
binding = "KV"
id = "xxxxxxxxxxxxxxxx"
Los bindings definidos aquí aparecen en c.env, totalmente tipados si los pasas como el genérico Bindings a new Hono<{ Bindings: Bindings }>(). Vercel, Deno Deploy y Netlify cuentan cada uno con un adaptador documentado, y normalmente la misma aplicación se despliega cambiando una sola línea de entrada. Recuerda las restricciones del runtime: limita las tareas que consuman mucha CPU, evita las conexiones a bases de datos de larga duración y utiliza almacenamiento nativo de edge.
Mejores prácticas
- Tipa los bindings y las variables a través de los genéricos de
Honopara quec.envyc.getsean seguros. - Valida cada entrada con
zValidatory mantén el esquema junto a la ruta. - Compón el middleware con
createMiddlewarey delimítalo mediante un patrón de ruta. - Exporta
AppTypey utilizahcen el cliente en lugar de escribir los tipos a mano. - Mantén los handlers ligeros; mueve la lógica reutilizable a funciones simples o servicios.
- Devuelve los códigos de estado correctos con
c.json(body, status)en lugar de enviar siempre un 200. - Diseña pensando en el runtime: sin sistema de archivos, CPU limitada y almacenamiento edge-native.
- Prueba con
app.request()para que los tests no necesiten servidor ni red.
test("GET /posts", async () => {
const res = await app.request("/posts");
expect(res.status).toBe(200);
});
Errores comunes
- Asumir que las librerías de Node funcionan en Workers; los módulos integrados y los complementos nativos a menudo no lo hacen.
- Mantener conexiones a la base de datos abiertas en un edge runtime en lugar de utilizar HTTP o bindings.
- Olvidar retornar la respuesta, haciendo que el handler se resuelva a
undefined. - Registrar
corsdespués de las rutas a las que debería aplicarse. - Leer
c.req.json()más de una vez, lo cual falla porque el stream del cuerpo ya ha sido consumido. - Omitir la validación y confiar en strings de
c.req.query(). - Permitir que
AppTypese desincronice al escribir manualmente los tipos del cliente en lugar de importarlos. - Realizar tareas pesadas de CPU durante una solicitud y alcanzar el límite de tiempo del runtime.
Próximos pasos
Hono lleva el modelo de routing y middleware que ya conoces de Express a runtimes que ni siquiera existían cuando se escribió Express. Si necesitas un servidor de Node de larga duración con validación basada en esquemas, compáralo con Fastify. Para entender los runtimes a los que se dirige Hono, lee la guía de conceptos básicos de Node.js, y cuando estés listo para lanzar a producción, el roadmap de backend cubre el despliegue en la nube. Después, construye una API pequeña, despliégala en Workers y llámala utilizando un cliente RPC tipado.