Spraxium logoSpraxium

Enrutamiento y Decoradores de Controladores

Entiende cómo los metadatos de rutas se convierten en handlers ejecutables en @spraxium/http. Sigue el flujo desde los decoradores hasta la resolución de parámetros, validación y forma de respuesta.

Superficie de decoradores

Los decoradores definen la intención de transporte en tiempo de carga de clase, mientras que los servicios runtime convierten esa intención en handlers Hono ejecutables durante el arranque del servidor. Mantener clara esa separación te ayuda a depurar rápido el comportamiento de rutas, porque el decorador siempre es la fuente de verdad y el runtime solo lee lo que el decorador guardó en reflect-metadata.

Todos los decoradores del paquete escriben en claves definidas en HTTP_METADATA_KEYS. @HttpController define el prefijo, los decoradores de método de ruta agregan entradas a un arreglo ROUTES, @HttpUseMiddleware agrega a un arreglo MIDDLEWARE, @HttpGuards agrega a un arreglo GUARDS y los decoradores de parámetro agregan a un arreglo PARAMS indexado por nombre de handler. Estos metadatos son inmutables tras la inicialización de la clase, por lo que el runtime puede leerlos en startup de forma segura y sin efectos secundarios.

Resolución de parámetros en detalle

ParamResolver es el servicio que lee metadatos PARAMS y extrae valores desde el Context de Hono para cada invocación de handler. Soporta cinco tipos de origen, cada uno mapeado a una estrategia de extracción específica.

Cuando un handler no tiene decoradores de parámetro, ParamResolver usa como fallback la inyección del Context completo como único argumento. Este comportamiento por defecto significa que puedes escribir handlers simples que acepten ctx directamente sin decoradores, útil en escenarios de migración o handlers que requieren control total. En cuanto aparece un decorador de parámetro, solo se resuelven parámetros decorados.

La fuente de body merece atención especial. ParamResolver parsea el JSON del body solo una vez por invocación de handler y cachea el resultado incluso si existen múltiples decoradores @HttpBody en el mismo método. Cuando se provee una clase DTO, ValidationPipe ejecuta plainToInstance de class-transformer para crear una instancia tipada y luego validate de class-validator con whitelist: true y forbidNonWhitelisted: true. Esto significa que propiedades inesperadas en el payload se rechazan automáticamente antes de ejecutar lógica de negocio.

Ejemplo enfocado con todas las fuentes de parámetros

import {
  HttpBody,
  HttpController,
  HttpCtx,
  HttpGet,
  HttpHeader,
  HttpParam,
  HttpPatch,
  HttpQuery,
  HttpStatus,
  NotFoundError,
} from '@spraxium/http';
import type { Context } from 'hono';
import { GuildHttpService } from '../services/guild-http.service';
import { UpdateGuildConfigDto } from '../dto/update-guild-config.dto';

@HttpController('/guilds')
export class GuildHttpController {
  constructor(private readonly service: GuildHttpService) {}

  @HttpGet('/:guildId')
  async getGuild(
    @HttpParam('guildId') guildId: string,
    @HttpQuery('includeMembers') includeMembers?: string,
    @HttpHeader('x-request-id') requestId?: string,
  ) {
    const guild = await this.service.getGuildSummary(guildId);
    if (!guild) throw new NotFoundError('Guild not found');
    return {
      ok: true,
      ...guild,
      includeMembers: includeMembers === 'true',
      requestId,
    };
  }

  @HttpPatch('/:guildId/config')
  @HttpStatus(200)
  async updateGuildConfig(
    @HttpParam('guildId') guildId: string,
    @HttpBody(UpdateGuildConfigDto) body: UpdateGuildConfigDto,
  ) {
    return this.service.updateConfig(guildId, body);
  }

  @HttpGet('/:guildId/raw')
  async getRawContext(@HttpCtx() ctx: Context) {
    const guildId = ctx.req.param('guildId');
    ctx.header('X-Custom-Header', 'spraxium');
    return ctx.json({ ok: true, guildId }, 200);
  }
}

Pipeline de validación de DTO

Cuando se usa @HttpBody(DtoClass), el body JSON crudo pasa por ValidationPipe antes de llegar al handler. El pipeline realiza dos transformaciones en secuencia y rechaza la request si falla la validación.

Primero, plainToInstance(DtoClass, body) de class-transformer convierte el objeto anónimo en una instancia de clase tipada. Este paso permite que class-validator lea metadatos de decoradores desde la clase DTO. Después, validate(instance, { whitelist: true, forbidNonWhitelisted: true }) ejecuta todos los decoradores de constraints sobre la instancia. La opción whitelist elimina propiedades desconocidas y forbidNonWhitelisted rechaza activamente requests que las incluyan.

Cuando hay errores de validación, ValidationPipe lanza un ValidationError con estado 400. Cada entrada de error en el arreglo details mapea un nombre de property a un registro de constraints. El handler centralizado onError captura esto y devuelve { ok: false, error: 'Validation failed', details: [...] }. Este flujo automático significa que los handlers nunca necesitan validar payloads manualmente, y cada endpoint de mutación obtiene formas de error consistentes sin esfuerzo adicional.

Secuencia de ejecución interna

Resolver grafo de servicios con orden topológico

RouteRegistry lee los tipos de parámetros del constructor desde reflect-metadata y construye un grafo de dependencias. Los servicios se instancian en orden para que cada clase reciba sus dependencias ya resueltas. Si se detecta una dependencia circular, el startup falla con un error que lista los nombres de clases involucradas.

Instanciar controladores con DI

Las clases de controlador se instancian usando el mapa de dependencias resuelto más cualquier valor fallback del contenedor core. RouteRegistry también lee en esta etapa metadatos de @HttpUseMiddleware y @HttpGuards de nivel clase para que estén disponibles durante el binding de rutas.

Enlazar handlers de rutas en Hono

RouteBuilder itera las definiciones de rutas de cada controlador. Para cada ruta, calcula el path completo uniendo el prefijo del controlador con el subpath del método. Fusiona referencias de middleware de nivel clase y método, las resuelve mediante middlewareProviders y registra la cadena completa como handler de ruta en Hono. El binding ocurre una sola vez en startup, haciendo inmutables los contratos de ruta durante runtime.

Resolver parámetros de método por request

Cuando una request coincide con una ruta, ParamResolver lee metadatos PARAMS de ese handler. Extrae valores de params URL, query strings, body parseado, headers o el context completo. Si un decorador @HttpBody referencia una clase DTO, se dispara ValidationPipe. El arreglo final de argumentos se pasa a la función handler mediante apply().

Ejecutar handler y dar forma a la respuesta

El handler se ejecuta con los argumentos resueltos. Si devuelve un Response nativo, Hono lo pasa sin cambios. En caso contrario, RouteBuilder serializa el valor retornado con ctx.json() usando el estado de @HttpStatus o 200 por defecto. Cualquier HttpError lanzado durante la ejecución se propaga al handler centralizado onError.

Reglas para dar forma a respuestas

Los handlers pueden devolver dos tipos de valores: objetos planos o instancias nativas de Response. Entender la diferencia evita confusión con códigos de estado y tipos de contenido.

Cuando un handler devuelve un objeto plano, RouteBuilder lo envuelve con ctx.json(result, statusCode) donde el código de estado proviene de @HttpStatus o usa 200 por defecto. El tipo de contenido se define automáticamente como application/json. Este es el patrón recomendado para la mayoría de endpoints porque mantiene a los handlers enfocados en datos y deja la serialización al framework.

Cuando un handler devuelve un Response nativo (construido con ctx.json(), ctx.text() o new Response(...)), Hono lo pasa sin modificación. Esto da control total sobre headers, estado y formato de body, útil para respuestas por streaming, descargas binarias o redirects. El costo es que el estado centralizado de @HttpStatus se ignora y la respuesta evita el camino normal de serialización JSON.

Trata los paths de ruta como contratos públicos

Publica versionado intencional en rutas cuando cambie el comportamiento de un endpoint. El runtime no detecta automáticamente colisiones de path, así que renombres silenciosos rompen clientes. Mantén prefijos de rutas estables y explícitos entre despliegues.