Spraxium logoSpraxium

Layouts y Mapeo

Comprende el rol de los layouts de excepción en el runtime de Spraxium y cómo se resuelven. Aprende cuándo usar el enfoque con decoradores, cuándo mapear en configuración y cuándo usar overrides por instancia.

Qué es un layout de excepción

Cuando un usuario ejecuta un comando y algo sale mal, lo que ve en el chat es el layout. Es la respuesta visual que Spraxium envía en nombre del error: un mensaje de texto, un embed con título y descripción, una respuesta efímera, o cualquier combinación de estas. La clase de excepción es la señal interna que indica qué falló y transporta los datos de contexto. El layout es la plantilla que consume ese contexto y lo convierte en el mensaje real de Discord que el usuario lee.

Esta separación significa que el código de dominio se mantiene enfocado en reglas de negocio y nunca necesita saber cómo se ven los errores para el usuario. Un guard que deniega acceso lanza una PermissionDeniedException. No construye un embed, no compone un string de mensaje ni decide si la respuesta debe ser efímera. Todo eso vive en el layout, que puede cambiarse u overridearse de forma independiente sin tocar ninguna lógica de dominio. El método build devuelve un objeto ExceptionLayoutPayload. Todos los campos son opcionales y se pueden combinar libremente. Un layout puede devolver solo contenido de texto, solo embeds, o una combinación con componentes interactivos.

CampoTipoPropósito
contentstringRespuesta de texto plano enviada como cuerpo del mensaje
embedsLista de entradas EmbedBuilder

Tarjetas embed enriquecidas con color, título, descripción y campos

componentsLista de entradas ActionRowBuilder

Componentes interactivos como botones o menús de selección

ephemeralboolean

Si la respuesta es visible solo para el usuario que la activó, por defecto true

flagsMessageFlags

Flags de bajo nivel de mensajes de Discord para control avanzado de respuesta

Orden de precedencia en runtime

Spraxium resuelve layouts en un orden fijo, y conocer este orden es esencial para evitar confusión cuando personalización local y configuración global coexisten en la misma aplicación. Cada nivel en la cadena de precedencia puede overridear al nivel inferior, lo que te da control fino sobre cómo se presentan los errores sin crear ambigüedad sobre qué layout va a renderizar realmente.

  1. Override por instancia en el objeto de excepción
  2. Mapeo de config que coincide con el nombre de la clase de excepción
  3. Decorador a nivel de clase con @WithLayout()
  4. La entrada por defecto en el mapa de layouts de configuración
  5. El DefaultExceptionLayout integrado del framework

Consecuencia práctica

Si mapeas una clase en el mapa de layouts de config, ese mapeo tiene prioridad sobre el decorador a nivel de clase, lo que permite que la configuración de la aplicación overridee defaults de librería cuando sea necesario.

Cuándo usar @WithLayout

Usa el decorador cuando la propia clase de excepción debe llevar un layout por defecto semántico como parte de su definición. Este enfoque funciona bien en paquetes compartidos o código de librería donde quieres que la clase se distribuya con un fallback razonable sin depender de la configuración de la aplicación consumidora. Los equipos que prefieren convenciones centradas en clases también se benefician de este patrón porque reduce la probabilidad de olvidar agregar un mapeo de layout al crear una nueva clase de excepción.

EscenarioPor qué encaja el decoradorTrade-off
Paquete compartido de excepcionesLa clase se distribuye con un layout por defecto opinadoLa app aún puede overridearlo en config
Convenciones de equipo centradas en clasesReduce la posibilidad de olvidar mapeosMenos política centralizada en un solo archivo

Cuándo mapear en configuración

Mapea en spraxium.config.ts cuando la política de layout está impulsada por decisiones de producto como branding, tono de lenguaje o estrategia específica por entorno. Esto suele ser la mejor opción por defecto para gobernanza a nivel aplicación porque centraliza toda la lógica de presentación de errores en un solo lugar, haciendo más fácil la gestión de cambios al poder intercambiar la política de layout sin modificar ninguna clase de excepción de dominio.

EscenarioPor qué encaja el mapeo en configTrade-off
UX de error con marcaCentraliza estilo, tono y política de fallbackRequiere disciplina en los mapeos
Política distinta por entornoPermite cambiar comportamiento sin tocar clasesLa config puede crecer mucho sin convenciones

Cuándo usar override por instancia

Usa el override por instancia solo para casos contextuales donde la misma clase de excepción debe renderizar distinto según condiciones de runtime. Ejemplos típicos incluyen comunicación temporal de incidentes, migraciones activas o experimentos con feature flags donde la respuesta de error debe cambiar sin modificar definiciones de clase ni archivos de configuración. Como este nivel tiene la prioridad más alta en la cadena de resolución, trátalo como una ruta de excepción intencional y documenta por qué existe para evitar comportamientos implícitos difíciles de rastrear durante debugging.

Ejemplo completo con embed personalizado

El siguiente ejemplo muestra cómo crear una clase de excepción con un layout a nivel de decorador y luego overridearlo en la configuración de aplicación. El mapeo en config tiene prioridad sobre el decorador, lo que significa que el layout con marca del equipo se renderiza en producción mientras que el decorador sirve como fallback para cualquier consumidor que no configure un mapeo específico.

policy.exception.ts
import { WithLayout } from '@spraxium/common';
import { defineExceptionLayout, SpraxiumException } from '@spraxium/core';
import { EmbedBuilder } from 'discord.js';

const PolicyLayout = defineExceptionLayout((exception) => ({
  embeds: [
    new EmbedBuilder()
      .setColor(0xffb020)
      .setTitle('Action blocked by policy')
      .setDescription(String(exception.props.reason ?? 'Reason not provided')),
  ],
  ephemeral: true,
}));

@WithLayout(PolicyLayout)
export class PolicyException extends SpraxiumException {
  constructor(props: { reason: string }) {
    super({
      code: 'POLICY_BLOCKED',
      message: 'Action blocked: {{reason}}',
      props,
    });
  }
}

Y en la configuración de la aplicación:

spraxium.config.ts
exceptions: {
  logUnhandled: true,
  layouts: {
    default: AppDefaultLayout,
    PolicyException: TeamBrandPolicyLayout,
  },
}

Regla de decisión recomendada

Al decidir qué enfoque usar, sigue este orden de preferencia para mantener una arquitectura de manejo de errores limpia y mantenible. Usa el mapa de layouts en config como política principal de tu aplicación, usa el decorador como fallback semántico a nivel de clase para librerías y paquetes compartidos, y reserva los overrides por instancia exclusivamente para comportamientos realmente contextuales que no puedan expresarse con los otros dos mecanismos.