← Volver al listado de tecnologías

Capítulo 2: Construyendo la Base con Hono - Rutas y Validación

Por: Tu Nombre
cloudflareworkershonoroutingvalidationzodsecuritytestingapivitest

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:
    // 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
    };
    
    Ahora, Hono se encarga de enrutar las peticiones al manejador correspondiente. 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 directorio src/routes y dentro un archivo echo.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, modificamos src/index.ts para importar y usar echoApp.

    // 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 cumple echoSchema, el middleware zValidator 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.
    // 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 };
    Este middleware app.use('*', ...) se ejecutará para todas las rutas (*). Llama a await 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 entorno fetch 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>