Hono: Framework para aplicaciones web

Cuando me enfrento a un nuevo proyecto, ya sea una API para una aplicación móvil o un backend para un sitio web, siempre busco herramientas que me hagan la vida más simple y fácil. No quiero mantener configuraciones complejas ni enfocar esfuerzos a optimizar código con un framework pesado que consuma más recursos de los necesarios. Mis prioridades son simples: quiero que sea rápido, ligero y que se integre sin problemas con Deno, que es el entorno de ejecución de TypeScript que he adoptado para casi todo.

Y ahí es donde entra Hono.

Descubrí Hono buscando una alternativa moderna a Express.js (un framework que usé durante años en el mundo de Node.js). Aunque Express es casi de facto el framework web para node.js, sentía que se estaba quedando desactualizado no encajaba del todo con la simplicidad y seguridad que me ofrecía Deno. Necesitaba algo que se sintiera más nativo, más ágil y moderno.

Lo que me convenció de Hono fue precisamente eso. Es increíblemente liviano, casi no añade overhead a mi aplicación, y su rendimiento es espectacular. Pero la verdadera joya de la corona, para mí, es su versatilidad. Aunque mi ecosistema principal es Deno, Hono me da la tranquilidad de saber que mi código es portable. Si el día de mañana decido mover una API a Cloudflare Workers para que corra en el edge (más cerca de los usuarios), o si un cliente me pide desplegarla en un servidor tradicional con Node.js, puedo hacerlo con cambios mínimos. Esa libertad es algo que valoro muchísimo.

Como vengo del mundo de Express, empezar con Hono fue muy sencillo. La forma de definir rutas, gestionar peticiones (req) y respuestas (res) y usar middlewares es tan intuitiva y familiar que no sentí que hiciera un esfuerzo adicional, es decir, si vienes del mundo Express, la curva de aprendizaje es rápida.

En este post quiero contarte, desde mi experiencia, por qué Hono se ha convertido en mi framework de cabecera para casi cualquier proyecto web que involucre un backend.

Manos a la Obra: Tu Primer Proyecto con Hono y Deno

Ya te conté por qué me gusta Hono, ahora vamos a la práctica para que veas lo rápido que es empezar.

Primero, abre tu terminal, crea una carpeta para el proyecto y entra en ella:

1
mkdir mi-proyecto-hono && cd mi-proyecto-hono

Deno tiene un comando muy práctico para inicializar proyectos que nos ahorra bastante trabajo. Usaremos el siguiente:

1
deno init

Este comando te creará una estructura básica con un main.ts y un deno.json. Ahora, para instalar Hono, agregamos la dependencia en el archivo deno.json y luego simplemente lo importamos en nuestro main.ts. Deno lo descargará y cacheará la primera vez que ejecutes el código.

Puedes agregar Hono como dependencia usando JSR de la siguiente forma, pero debes editar el archivo deno.json para corregir el alias en el import.

1
deno add jsr:@hono/hono

El corazón de tu proyecto - deno.json

Antes de ver el código, echemos un vistazo al deno.json. Aquí es donde Deno gestiona las dependencias y tareas. Para usar Hono, podemos añadirlo a la sección imports o usar la línea de comandos de deno para agregar la dependencia:

1
2
3
4
5
6
7
8
{
"imports": {
"hono": "jsr:@hono/hono@^4.10.3" // <-- ojo con el alias
},
"tasks": {
"start": "deno run --allow-net main.ts"
}
}

Un detalle importante es que, como ves, Hono está disponible en JSR (el nuevo registro de paquetes de JavaScript), lo que hace que manejar las versiones sea muy limpio.

Primer controlador

Ahora sí, el código de main.ts. Es tan simple y elegante como esto:

1
2
3
4
5
6
7
8
9
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
return c.text("Hello Hono!");
});

Deno.serve(app.fetch);

¿Qué hace este código?

  1. Importa Hono.
  2. Crea una nueva instancia de la aplicación.
  3. Define una ruta para el método GET en la URL raíz (/).
  4. Cuando alguien visita esa ruta, le devuelve el texto “Hello Hono!”.
  5. Finalmente, Deno.serve inicia el servidor.

Para ponerlo en marcha, usamos la tarea que definimos en deno.json:

1
deno task start

Como Deno es seguro por defecto, te pedirá permiso para acceder a la red. ¡Y listo! Ya tienes un servidor corriendo. Si usas una herramienta como httpie, verás esto:

1
2
3
4
5
6
$ http :8000/

HTTP/1.1 200 OK
...

Hello Hono!

¿Y si quiero devolver JSON?

Fácil. Hono tiene un método específico para eso. Simplemente cambia c.text() por c.json():

1
2
3
app.get("/", (c) => {
return c.json({ message: "Hello Hono!" });
});

El resultado ahora será una respuesta JSON perfecta:

1
2
3
4
5
6
7
8
9
$ http :8000/

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
...

{
"message": "Hello Hono!"
}

En solo unos minutos, hemos montado un servidor web funcional. Esta simplicidad y rapidez es lo que me terminó de convencer de la combinación de Hono y Deno.

¿ Cómo cambiar el puerto del servidor ?

Esto me costó pillarlo en su momento, así que dejo aquí cómo se hace:

1
Deno.serve({ port: 8080 }, app.fetch);

Revisemos algunos middlewares útiles

logger

Un middleware bien útil es el de logger, permite registrar (o loggear) lo que entra y lo que sale a Hono (obviamente depende de la ubicación en el archivo principal de la aplicación.)

1
2
3
4
5
6
import { Hono } from "hono";
import { logger } from "hono/logger";

const app = new Hono();

app.use(logger());

Con esta configuración se puede obtener este tipo de logs.

1
2
3
Listening on http://0.0.0.0:8000/ (http://localhost:8000/)
<-- GET /
--> GET / 200 0ms

etag

La cabecera o header ETag (que viene de Entity Tag o “etiqueta de entidad”) es básicamente una huella digital o un identificador único que el servidor le asigna a una versión específica de un recurso (como una página, una imagen o un archivo JSON).

Imagina que tienes un archivo en tu servidor. Cada vez que modificas y guardas ese archivo, su ETag cambia. Es como el número de versión de ese recurso en un momento exacto.

¿Y para qué sirve?

Su propósito principal es hacer que el cacheo sea mucho más eficiente y ahorrar ancho de banda. El flujo es el siguiente:

  1. Primera Visita: Cuando tu navegador pide un recurso (ej: styles.css) por primera vez, el servidor se lo envía completo y, además, incluye el header ETag con su “huella digital”. Por ejemplo: ETag: “v1-a2b3c4d5”.
  2. El Navegador Guarda la Huella: Tu navegador guarda el archivo styles.css en su caché y también anota su ETag.
  3. Siguientes Visitas: La próxima vez que necesite ese mismo archivo, el navegador no lo pide a ciegas. En su lugar, le pregunta al servidor: “Oye, tengo una versión de styles.css con la huella v1-a2b3c4d5. ¿Sigue siendo la buena?”. Esto lo hace enviando un header llamado If-None-Match: “v1-a2b3c4d5”.
  4. El Servidor Decide:
    • Si la “huella digital” del archivo en el servidor coincide con la que envió el navegador, significa que el archivo no ha cambiado. El servidor responde con un 304 Not Modified. Este mensaje es muy liviano, no contiene el archivo, y básicamente le dice al navegador: “Todo sigue igual, usa la copia que ya tienes en tu caché”.
    • Si la “huella digital” no coincide, significa que el archivo fue actualizado. El servidor responde con un 200 OK, envía la nueva versión del archivo y, por supuesto, su nuevo ETag.

En resumen, el ETag permite al navegador evitar volver a descargar recursos que no han cambiado, haciendo que la navegación sea mucho más rápida y consuma menos datos. Además ayuda a las capas superiores de la aplicación a tomar mejores decisiones respecto del cache de la aplicación.

Ahora la configuración para Hono:

1
2
3
4
5
6
import { Hono } from "hono";
import { etag } from "hono/etag";

const app = new Hono();

app.use(etag());

Finalmente esta será la respuesta donde aparece una cabecera con el etag.

1
2
3
4
5
6
7
8
9
10
11
http :8000/
HTTP/1.1 200 OK
content-length: 25
content-type: application/json
date: Tue, 28 Oct 2025 03:25:31 GMT
etag: "6e488564c25da2b4d7326d5ebfa92e957af61dcb"
vary: Accept-Encoding

{
"message": "Hello Hono!"
}

cors

Probablemente, uno de los “problemas” más comunes al empezar a desarrollar una API es toparse con un error de CORS en el navegador.

Imagina que los sitios web son como reinos amurallados. Por seguridad, un reino (tudominio.com) no puede simplemente enviar un mensajero a pedirle recursos a otro reino (api.otrodominio.com). Esta regla se llama Política del Mismo Origen (Same-Origin Policy) y es fundamental para la seguridad en la web.

El middleware de CORS de Hono es el pasaporte diplomático que nos permite configurar qué “reinos” externos tienen permiso para comunicarse con nuestra API.

Por ejemplo, una configuración de CORS muy específica y segura podría ser así:

1
2
3
4
5
6
7
8
9
10
11
import { Hono } from 'hono';
import { cors } from 'hono/cors';

app.use('/api/*', cors({
origin: 'http://example.com',
allowMethods: ['POST', 'GET', 'OPTIONS'],
allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
maxAge: 600,
credentials: true,
}));

Analicemos las reglas que hemos definido en este bloque:

  • app.use('/api/*', ...): Primero, le indicamos a Hono que aplique este middleware únicamente a las rutas que comiencen con /api/. Esto es muy práctico para tener políticas de seguridad distintas para diferentes partes de tu aplicación.
  • origin: 'http://example.com': La regla más importante. Se especifica que solo las peticiones que vengan desde http://example.com están permitidas.
  • allowMethods: [...]: Define qué métodos HTTP están autorizados (POST, GET, OPTIONS). Un DELETE sería bloqueado.
  • allowHeaders: [...]: Una lista blanca de las cabeceras HTTP que el cliente tiene permitido enviar.
  • exposeHeaders: [...]: Permite que el código JavaScript del cliente pueda leer cabeceras de la respuesta que, por defecto, el navegador oculta por seguridad, como Content-Length.
  • maxAge: 600: Una optimización de rendimiento. Indica al navegador que puede guardar en caché el resultado de la petición de “verificación” (preflight) durante 600 segundos (10 minutos).
  • credentials: true: Esencial si tu frontend necesita enviar credenciales como cookies o cabeceras de autenticación (Authorization).

En resumen, este middleware te da un control muy granular para configurar una política de CORS segura, una práctica mucho más recomendable que simplemente abrir el acceso a todo el mundo con origin: '*'.

Palabras al Cierre

Como hemos visto en este recorrido, Hono se presenta como una alternativa muy atractiva en el ecosistema de JavaScript y TypeScript. Su filosofía minimalista, su rendimiento y, sobre todo, su capacidad para adaptarse a cualquier entorno de ejecución, lo convierten en una herramienta que vale la pena tener en nuestro arsenal.

Personalmente, la combinación de la simplicidad de Hono con la seguridad y el entorno moderno de Deno ha hecho que desarrollar APIs vuelva a ser una experiencia ágil y productiva.

Pero esto es solo el comienzo. Este artículo es el primero de una serie donde exploraremos a fondo el ecosistema de Deno y Hono. En las próximas entregas, veremos temas un poco más avanzados como:

  • Creación de nuestros propios middlewares.
  • Helpers de Hono
  • Rutas avanzadas y manejo de grupos.
  • Estrategias de testing para nuestras APIs.

Te invito a que experimentes con lo que hemos visto hoy y, por supuesto, a que te mantengas atento para los próximos artículos de la serie.


Referencias Útiles

Aquí te dejo una lista de enlaces que te serán de gran ayuda para profundizar en Hono y Deno:

TodoList Con Node.js Y MongoDB - Parte II

Lo prometido es deuda, aquí vamos con la segunda parte donde integraremos Passport para poder validar los request y contronlar sesiones de usuarios.

Primero debemos agregar los siguientes imports al archivo app.js y la estrategia (plugin de autenticación) a utilizar. Además se agregan los archivos UserModel.js y security.js extensión del modelo de User y métodos de validación del request respectivamente:

1
2
3
4
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var security = require('./config/security');
var User = require('./model/UserModel');

Ahora configuramos Express para que pueda utilizar Passport como administrador de sesiones de usuario. Es necesario que express use su propio administrador de sesiones y además debe ser inicializado antes que el de passport.

1
2
3
4
5
6
app.use(express.cookieParser('your secret here'));
app.use(express.session());

// passport initialize
app.use(passport.initialize());
app.use(passport.session());

Luego agregamos mecanismos de autenticación, serialización y deserialización a passport:

1
2
3
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

Model

Ahora extendemos el modelo de usuario para agregar nuestros atributos (si es es necesario, por ahora lo dejaremos vacío):

1
2
3
4
5
6
7
8
9
10
var mongoose = require('mongoose');
var passportLocalMongoose = require('passport-local-mongoose');

var UserSchema = new mongoose.Schema({});

UserSchema.plugin(passportLocalMongoose);

var UserModel = mongoose.model('User', UserSchema);

module.exports = UserModel;

Seguridad

Hay un archivo que se llama security.js que contiene una función que valida si el request solicitado está autenticado por passport, si está autenticado lo “deja pasar” (next), de lo contrario lo redirecciona al template de login.

1
2
3
4
5
6
exports.ensureAuthenticated = function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
};

Nos queda asegurar las urls o recursos que necesitemos con la función ensureAuthenticated en el archivo app.js. La idea es que sea lo menos intrusivo posible. Hay dos formas de hacerlo, la primera es intrusiva (y no recomendada aunque puede utilizarse con fines específicos) y se debe realizar por cada request en los archivos que contienen los Controllers:

1
2
3
4
5
6
7
exports.authenticationExample = function(req, res) {
if (req.isAuthenticated()) {
// Do something
} else {
res.redirect('/login');
}
}

La segunda es interceptando los request y validando según algún patrón, que es nuestro caso. Esta configuración esta en app.js donde se declaran las rutas a los Controllers:

1
2
app.all('/api/*', security.ensureAuthenticated);
app.all('/todos', security.ensureAuthenticated);

Con esto interceptamos todos los request que van a la API y los que van a /todos y los hacemos pasar por la función de autenticación de request. Con esto evitamos poner dichas líneas de autenticación en cada controller.

Login y Logout

Finalmente nos queda la última parte respecto de la autenticación y es configurar el login y el logout. Para el login usamos la API de passport y delegamos el trabajo de autenticación de la siguiente forma (configuración):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Funcion sin autenticación que permite desplegar el template de Login
*/
app.get('/login', function(req, res) {
res.render('login', {});
});

/**
* Función de login que permite delegar la autenticacion en la API de
* passport, se configura además cuando la autenticación es exitosa a que url
* lo debe redireccinar y lo mismo en caso de error.
*/
app.post('/login', passport.authenticate('local', {
successRedirect: '/todos',
failureRedirect: '/login'
}));

El logout es más sencillo y lo único que hay que hacer es invalidar el request, destruir la sesión del usuario y redireccionar al login.

1
2
3
4
app.get('/logout', function(req, res) {
req.logout();
res.redirect('/login');
});

Me queda pendiente en la próxima parte del tutorial explicar cómo consumir estos servicios usando angular y desplegar los templates.

TodoList con Node.js y MongoDB - Parte I

Llevo un tiempo estudiando y haciendo algunas cosas con Node.js y me he llevado una grata impresión con esta plataforma. Al principio cuesta un poco entender el cómo funciona ya que el paradigma es muy distinto del que he venido usando durante este último tiempo.

Me animé y empece a usar Node.js para hacer algo simple y así tratar de entender el cómo funciona. Para hacer algo mas sabrosa la experiencia agregué algunos ingredientes adicionales, un framework para desarrollo web llamado Express, Mongoose para modelar los documentos que van a ir a parar a MongoDB, Passport para autenticar los request (control de usuarios y permisos), uso de templates con EJS y finalmente para la vista usamos el framework AngularJS. Como podrán ver es un stack de tecnologías basadas en Javascript.

Para iniciar voy a explicar algunas partes del código que personalmente me costó entender e implementar. Algunos de esos puntos fueron la integración con passport para la autenticación de los request, control de usuario y uso de datos en sesión.

Configuración de node.js

Cuando utilizamos nodejs siempre hay un archivo principal donde se configura toda la plataforma, en este caso el archivo es llamado app.js. Voy a ir mostrando ciertas partes del código (si quieren ver el código, esta disponible en github):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');

var indexController = require('./routes/IndexController');
var todosController = require('./routes/TodosController');
var registerController = require('./routes/RegisterController');

var http = require('http');
var path = require('path');
var app = express();
var db = require('./config/database');


var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

var security = require('./config/security');
var User = require('./model/UserModel');

Controllers

Lo primero es incluir el módulo de express y luego los Controllers de la aplicación. Veamos el código de IndexController

1
2
3
4
5
exports.index = function(req, res) {
res.render('todos', {
user: req.user
});
};

Este es el Controller mas simple de la aplicación, lo único que hace es exponer mediante exports la función index cuya responsabilidad es desplegar la página principal de la aplicación. Lo demás está en el controlador de la API que será llamada desde dicha página (usando ajax).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var Todo = require('../model/TodosModel');

/**
* Busca todos los todos para el usuario registrado
* @param {Object} req request
* @param {Object} res response
*/
function findAllTodosByUser(req, res) {
var userId = req.user.id;

Todo.find()
.where('creator')
.equals(userId)
.sort('date')
.exec(function(err, todos) {
if (err) {
res.send(err);
}
res.json(todos);
});
}

exports.allTodos = findAllTodosByUser;

/**
* Crea un todo para el usuario
* @param {Object} req request
* @param {Object} res response
* @return {Object} Lista de todos
*/
exports.createTodo = function(req, res) {
var userId = req.user.id;
var textTodo = req.body.text;

Todo.create({
text: textTodo,
done: false,
creator: userId
}, function(error, todo) {
if (error) {
res.send(error);
}
findAllTodosByUser(req, res);
});
}

/**
* Elimina un Todo por su ID
* @param {Object} req request
* @param {Object} res response
* @return {Object} Lista de todos
*/
exports.deleteTodo = function(req, res) {
Todo.remove({
_id: req.params.todo_id
}, function(error, todo) {
if (error) {
res.send(error);
}
findAllTodosByUser(req, res);
});
}

Este controlador expone la API REST con la que se comunicará la pagina principal, haciendo llamadas ajax a los distintos métodos. En esta pieza se hace un require de otro script que representa el Model, en este caso es un document para MongoDB.

Model con mongoose

Aquí mostraré como se modeló el documento principal de la aplicación Todo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mongoose = require('mongoose');

var TodoSchema = new mongoose.Schema({
text: String,
date: {
type: Date,
default: Date.now
},
done: {
type: Boolean,
default: false
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
});

var TodoModel = mongoose.model('Todo', TodoSchema);

module.exports = TodoModel;

Primero se incluye el módulo de mongoose que nos permitirá modelar los documentos para MongoDB. Luego se crea un schema para modelar y se le agregan los atributos que va a poseer y sus respectivas validaciones. Por ejemplo, el atributo date es de tipo Date y valor por omisión la fecha de hoy.

Además este schema contiene una referencia a otra Collection para dejar relacionado el usuario creador en sus Todos.

Route y manejo de URLs

Finalmente para que la aplicación funcioné, hay que indicarle a node.js que las urls que se soliciten hay que enviarlas a alguien que las atienda, en este caso, los controladores y sus funciones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app.get('/login', function(req, res) {
res.render('login', {});
});

app.post('/login', passport.authenticate('local', {
successRedirect: '/todos',
failureRedirect: '/login'
}));

app.get('/logout', function(req, res) {
req.logout();
res.redirect('/login');
});

app.get('/', function(req, res) {
res.redirect('/login')
})

app.get('/register', registerController.index);
app.post('/register', registerController.registerUser);

app.all('/api/*', security.ensureAuthenticated);
app.all('/todos', security.ensureAuthenticated);

app.get('/todos', indexController.index);
app.get('/api/todos', todosController.allTodos);
app.post('/api/todos', todosController.createTodo);
app.delete('/api/todos/:todo_id', todosController.deleteTodo);

No hay mucho que explicar, a buen programador pocas lineas de código, las urls que ahi aparecen son ruteadas a los controladores o se implementa el callback inline, como por ejemplo las urls de login y logout. Los controller que digan security o passport, serán materia del próximo post.

Quedan algunos temas sin tocar en este capítulo, falta que revisemos la integración con AngularJS, el despliegue de los templates con EJS y la integración con passport para autenticar y autorizar los request.

Si tienen preguntas, bienvenidas sean.