Capítulo 2: Construyendo la Base con Hono - Rutas y Validación
Capítulo 2: Construyendo la Base con Hono: Rutas y Validación
En el capítulo anterior, configuramos nuestro entorno y creamos un Worker básico. Ahora, introduciremos Hono, un framework web ultrarrápido y ligero diseñado específicamente para entornos Edge como Cloudflare Workers. Hono simplificará enormemente la creación de rutas, el manejo de peticiones y la implementación de middleware.
< Volver al Índice del Tutorial
Introducción a Hono: Características de Seguridad Integradas
Hono destaca por su rendimiento y tamaño mínimo, pero también por venir preparado para escribir código seguro.
-
¿Por qué Hono?
- Rendimiento: Es uno de los routers más rápidos disponibles para JavaScript.
- Ligero: Su núcleo es muy pequeño, ideal para la runtime de Workers.
- API Familiar: Si has trabajado con frameworks como Express o Koa, te sentirás cómodo.
- Tipado Fuerte: Excelente integración con TypeScript.
- Middleware: Sistema de middleware sencillo y potente.
- Multi-runtime: Funciona en Workers, Deno, Bun, Node.js, etc.
-
Características de Seguridad:
- Hono no incluye funcionalidades inseguras por defecto.
- Facilita la integración con librerías de validación como Zod.
- Su sistema de middleware permite añadir capas de seguridad fácilmente (CORS, Headers, Rate Limiting).
-
Instalación: Añade Hono a tu proyecto:
bun add hono
Creando Rutas Básicas (GET, POST)
Vamos a refactorizar nuestro src/index.ts
para usar Hono y definir nuestras primeras rutas.
- Refactorizando
src/index.ts
:
Ahora, Hono se encarga de enrutar las peticiones al manejador correspondiente.// src/index.ts import { Hono } from 'hono'; // Define la interfaz Env como en el capítulo anterior interface Env { // MI_SECRETO_API?: string; // MI_KV?: KVNamespace; // MI_DB?: D1Database; } // Inicializa la aplicación Hono // El tipo genérico <{ Bindings: Env }> proporciona tipos correctos para c.env const app = new Hono<{ Bindings: Env }>(); // Ruta GET simple en la raíz app.get('/', (c) => { // 'c' es el contexto de Hono, similar al ctx anterior pero con más helpers. // c.text, c.json, c.html son helpers para crear respuestas. console.log(`[${c.req.method}] ${c.req.url} - Petición a la raíz`); return c.text('¡Bienvenido a la API con Hono!'); }); // Ruta POST simple (ej: /echo) app.post('/echo', async (c) => { try { const body = await c.req.json(); // Lee el body como JSON console.log(`[${c.req.method}] ${c.req.url} - Recibido:`, body); // Devuelve el mismo JSON recibido como respuesta return c.json({ message: 'Recibido correctamente', data: body, }); } catch (error) { console.error('Error procesando JSON:', error); return c.json({ error: 'Cuerpo de la petición inválido o no es JSON' }, 400); } }); // Ruta para manejar cualquier método no definido (404) app.notFound((c) => { return c.json({ error: 'Ruta no encontrada' }, 404); }); // Exporta el manejador fetch de Hono export default { fetch: app.fetch, // Hono maneja el objeto request, env, ctx internamente };
app.fetch
es la función que cumple la interfaz esperada por la runtime de Workers.
Organizando Rutas: Modularización
A medida que tu API crece, mantener todas las rutas en src/index.ts
se vuelve difícil de manejar. Hono permite dividir tu aplicación en módulos más pequeños. Cada módulo puede ser una instancia separada de Hono que exporta sus propias rutas. Luego, la aplicación principal importa y monta estas sub-aplicaciones bajo un prefijo de ruta específico usando app.route()
.
-
Creando un Módulo de Rutas (
src/routes/echo.ts
): Primero, creemos un directoriosrc/routes
y dentro un archivoecho.ts
. Este archivo manejará específicamente la lógica relacionada con la ruta/echo
.// src/routes/echo.ts import { Hono } from 'hono'; import { z } from 'zod'; import { zValidator } from '@hono/zod-validator'; import type { Env } from '../index'; // Importar o definir el tipo Env si es necesario aquí const echoApp = new Hono<{ Bindings: Env }>(); const echoSchema = z.object({ message: z.string().min(1, { message: "El mensaje no puede estar vacío" }), priority: z.number().int().positive().optional(), }); echoApp.post( '/', // La ruta ahora es '/' relativa a '/echo' zValidator('json', echoSchema, (result, c) => { if (!result.success) { console.error('Error de validación (echo):', result.error.issues); return c.json({ error: 'Datos inválidos.', details: result.error.flatten().fieldErrors }, 400); } }), async (c) => { const validatedData = c.req.valid('json'); console.log(`[${c.req.method}] ${c.req.path} (sub-app) - Validado:`, validatedData); return c.json({ message: `Echo: ${validatedData.message}`, priority: validatedData.priority ?? 'N/A', }); } ); export default echoApp;
Nota que la ruta ahora es
echoApp.post('/', ...)
porque su prefijo (/echo
) se definirá en la aplicación principal. -
Montando el Módulo en la Aplicación Principal (
src/index.ts
): Ahora, modificamossrc/index.ts
para importar y usarechoApp
.// src/index.ts import { Hono } from 'hono'; import echoApp from './routes/echo'; // Importar la sub-aplicación export interface Env { ... } // Definir Env aquí const app = new Hono<{ Bindings: Env }>(); app.get('/', (c) => c.text('¡Bienvenido a la API con Hono!')); // Montar la sub-aplicación 'echoApp' bajo la ruta '/echo' app.route('/echo', echoApp); app.notFound((c) => c.json({ error: 'Ruta no encontrada' }, 404)); export default { fetch: app.fetch };
¡Así de simple! Ahora la lógica de
/echo
está encapsulada en su propio archivo. Puedes repetir este patrón para diferentes recursos (ej:src/routes/tasks.ts
,src/routes/users.ts
).
Seguridad: Validación Estricta de Entradas (Zod)
Una de las vulnerabilidades más comunes es aceptar datos de entrada no validados. Hono se integra perfectamente con librerías como Zod para definir esquemas y validar datos automáticamente usando middleware.
-
Instalación de Zod y Middleware Hono/Zod:
bun add zod @hono/zod-validator
-
Ejemplo: Validación en Ruta POST: Modifiquemos la ruta
/echo
para que espere un JSON con una estructura específica y la valide.// src/index.ts import { Hono } from 'hono'; import { z } from 'zod'; // Importar Zod import { zValidator } from '@hono/zod-validator'; // Importar middleware interface Env { /* ... */ } const app = new Hono<{ Bindings: Env }>(); // Definir el esquema de validación con Zod const echoSchema = z.object({ message: z.string().min(1, { message: "El mensaje no puede estar vacío" }), // String no vacío priority: z.number().int().positive().optional(), // Número entero positivo opcional }); app.get('/', (c) => c.text('¡Bienvenido a la API con Hono!')); // Aplicar el middleware zValidator a la ruta POST // 'json' indica que validará el c.req.json() usando echoSchema app.post( '/echo', zValidator('json', echoSchema, (result, c) => { // Hook opcional para manejar errores de validación personalizados if (!result.success) { console.error('Error de validación:', result.error.issues); return c.json({ error: 'Datos inválidos proporcionados.', details: result.error.flatten().fieldErrors // Devuelve errores por campo }, 400 // Bad Request ); } }), // Este manejador SOLO se ejecuta si la validación fue exitosa async (c) => { // Los datos validados están disponibles en c.req.valid('json') const validatedData = c.req.valid('json'); console.log(`[${c.req.method}] ${c.req.url} - Validado:`, validatedData); return c.json({ message: `Mensaje recibido: ${validatedData.message}`, priority: validatedData.priority ?? 'No especificada', // Usar datos validados }); } ); app.notFound((c) => c.json({ error: 'Ruta no encontrada' }, 404)); export default { fetch: app.fetch };
Ahora, si envías un POST a
/echo
con un JSON que no cumpleechoSchema
, el middlewarezValidator
interceptará la petición y devolverá un error 400 detallado antes de que llegue a tu lógica principal. ¡Esto es seguridad proactiva!
Manejo Seguro de Peticiones y Respuestas (Headers)
Es buena práctica añadir headers de seguridad a tus respuestas para mitigar ciertos tipos de ataques (XSS, clickjacking, etc.). Hono facilita esto con middleware o directamente en la respuesta.
- Middleware de Headers de Seguridad (Ejemplo Básico):
Puedes crear un middleware simple para añadir headers comunes a todas las respuestas.
Este middleware// src/index.ts // ... (imports y setup de Hono/Zod) ... // Middleware para añadir Headers de Seguridad app.use('*', async (c, next) => { // Ejecuta el resto de la cadena de middleware y rutas await next(); // Modifica la respuesta ANTES de enviarla c.res.headers.set('X-Content-Type-Options', 'nosniff'); c.res.headers.set('X-Frame-Options', 'DENY'); c.res.headers.set('X-XSS-Protection', '1; mode=block'); // Considera añadir CSP (Content-Security-Policy) para mayor seguridad }); // ... (tus rutas GET, POST, etc.) ... export default { fetch: app.fetch };
app.use('*', ...)
se ejecutará para todas las rutas (*
). Llama aawait next()
para pasar el control y, una vez que la ruta principal ha generado una respuesta (c.res
), añade los headers antes de devolverla.
Middlewares en Hono (Logging Seguro, CORS)
Hono brilla por su sistema de middleware. Además de la validación y los headers, podemos usarlo para logging y CORS.
-
Logging Seguro: Podemos crear un logger simple, asegurándonos de no loguear información sensible.
// src/index.ts // ... (imports) ... const app = new Hono<{ Bindings: Env }>(); // Middleware de Logging app.use('*', async (c, next) => { const start = Date.now(); await next(); // Espera a que la ruta y otros middlewares terminen const ms = Date.now() - start; // Log seguro: Método, URL, Status, Tiempo. Evita loguear body o headers sensibles. console.log(`${c.req.method} ${c.req.url} - ${c.res.status} [${ms}ms]`); }); // Middleware de Headers de Seguridad (del ejemplo anterior) // ... // Rutas (GET, POST con validación) // ... export default { fetch: app.fetch };
-
CORS Restrictivo: Si tu API será consumida por un frontend en un dominio diferente, necesitas configurar CORS. Hono tiene un middleware oficial para esto.
bun add @hono/cors
// src/index.ts import { Hono } from 'hono'; import { cors } from 'hono/cors'; // Importar middleware CORS // ... (otros imports) ... const app = new Hono<{ Bindings: Env }>(); // Middleware de Logging // ... // Middleware CORS - ¡Configúralo de forma restrictiva! app.use( '/api/*', // Aplicar solo a rutas bajo /api/ (o tu prefijo) cors({ origin: 'https://tu-frontend.com', // Especifica el origen permitido EXACTO allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Métodos permitidos allowHeaders: ['Content-Type', 'Authorization'], // Headers permitidos maxAge: 600, // Tiempo de cache para preflight // credentials: true, // Solo si necesitas cookies/auth headers complejos }) ); // Middleware de Headers de Seguridad // ... // Rutas (GET, POST bajo /api/ ?) // ... (Asegúrate que tus rutas usen el prefijo si aplicaste CORS así) export default { fetch: app.fetch };
Seguridad CORS: Es crucial ser lo más restrictivo posible con
origin
. Evita usar*
a menos que sea absolutamente necesario y entiendas las implicaciones.
Testing: Pruebas Unitarias para Rutas y Validadores
Ahora que usamos Hono, podemos probar nuestras rutas y validadores de forma aislada usando el cliente app.request
de Hono, que funciona perfectamente con el entorno de testing de Workers/Miniflare.
-
Actualizando
test/index.test.ts
:// test/index.test.ts import { describe, it, expect } from 'vitest'; // ¡Ya no necesitamos importar 'env', 'createExecutionContext', etc. directamente! // Importamos nuestra instancia de la app Hono // ¡Importante! Asegúrate que tu src/index.ts exporte la app para testing, // o exporte el objeto default { fetch: app.fetch } // Si exportas default, puedes necesitar importar así: import appHandler from '../src/index'; const app = { fetch: appHandler.fetch }; // Para este ejemplo, asumimos que src/index.ts exporta la instancia 'app' directamente o su handler fetch. // Ajusta la importación según tu estructura real. import worker from '../src/index'; // Asume export default { fetch: app.fetch } const app = { fetch: worker.fetch }; // Adaptador simple si es necesario describe('API Routes Test', () => { it('GET / debería responder con el mensaje de bienvenida y status 200', async () => { // Usamos app.fetch para simular una petición a la ruta '/' const response = await app.fetch(new Request('http://example.com/')); expect(response.status).toBe(200); expect(await response.text()).toBe('¡Bienvenido a la API con Hono!'); }); it('POST /echo con datos válidos debería responder 200 y mensaje correcto', async () => { const payload = { message: 'Hola Hono', priority: 1 }; const request = new Request('http://example.com/echo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const response = await app.fetch(request); // Comprobar que el status es 200 antes de intentar parsear JSON expect(response.status).toBe(200); const responseBody = await response.json(); expect(responseBody.message).toBe('Mensaje recibido: Hola Hono'); expect(responseBody.priority).toBe(1); }); it('POST /echo sin mensaje (inválido) debería responder 400', async () => { const payload = { priority: 2 }; // Falta 'message' const request = new Request('http://example.com/echo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const response = await app.fetch(request); // Comprobar que el status es 400 expect(response.status).toBe(400); const responseBody = await response.json(); expect(responseBody.error).toBe('Datos inválidos proporcionados.'); // Verificar que el detalle del error menciona el campo 'message' expect(responseBody.details?.message).toContain('El mensaje no puede estar vacío'); }); it('GET /ruta-inexistente debería responder 404', async () => { const response = await app.fetch(new Request('http://example.com/ruta-inexistente')); // Comprobar que el status es 404 expect(response.status).toBe(404); const responseBody = await response.json(); expect(responseBody.error).toBe('Ruta no encontrada'); }); // Añadir tests para CORS, Headers de Seguridad si es necesario });
Ahora las pruebas son más limpias, interactuando directamente con la instancia de Hono (
app.fetch
) en lugar de simular manualmente el entornofetch
de Workers.
Hemos introducido Hono, creado rutas, implementado validación estricta con Zod, añadido headers de seguridad, configurado CORS y adaptado nuestras pruebas. En el próximo capítulo, nos sumergiremos en la persistencia de datos con D1 y Drizzle ORM.
< Volver al Índice del Tutorial --- Capítulo 3: Persistencia Segura de Datos >
</rewritten_file>