La clase handler
Una clase handler es una clase independiente decorada con @SlashCommandHandler que contiene la lógica de ejecución de un slash command. El decorador recibe la clase de definición del comando como primer argumento, creando un enlace de metadatos entre el esquema y la lógica. La clase handler se instancia mediante el contenedor DI durante la carga del módulo, por lo que los parámetros del constructor se resuelven automáticamente desde los providers del módulo.
El handler debe exponer un método handle. Cuando llega una interacción que coincide con el nombre del comando (y opcionalmente con subcomando y grupo), el SlashInvoker llama a este método después de que el pipeline de guards se aprueba. La lista de parámetros de handle se construye dinámicamente a partir de metadatos: @Ctx() inyecta la interacción en bruto, los decoradores de opción inyectan valores de opción resueltos, y cualquier parámetro sin decorador recibe undefined.
import { Ctx, SlashCommandHandler, SlashStringOption } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { PingCommand } from '../commands/ping.command';
@SlashCommandHandler(PingCommand)
export class PingHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashStringOption('service') service: string | null,
): Promise<void> {
const ws = interaction.client.ws.ping;
if (service) {
await interaction.reply(`Service: ${service}, Latency: ${ws}ms`);
} else {
await interaction.reply(`Pong! ${ws}ms`);
}
}
}Spraxium exporta un tipo SlashContext desde @spraxium/common que es un alias directo de ChatInputCommandInteraction. Importar desde @spraxium/common reduce el acoplamiento directo con discord.js en tus archivos handler. Ambos tipos son intercambiables; usa la ruta que mejor encaje con la organización de imports de tu equipo.
El decorador @SlashCommandHandler
El decorador @SlashCommandHandler acepta un argumento requerido y uno opcional. El primer argumento es la clase decorada con @SlashCommand a la que pertenece este handler. El segundo argumento es un objeto de opciones con propiedades sub y group para enrutar a subcomandos específicos.
| Argumento | Tipo | Requerido | Descripción |
|---|---|---|---|
| command | Constructor de clase | Sí | La clase decorada con @SlashCommand para la que ejecuta este handler. |
| options.sub | string | No | El nombre del subcomando al que responde este handler. Omítelo para handlers del comando raíz. |
| options.group | string | No | El nombre del grupo de subcomandos al que responde este handler. Debe usarse junto con sub. |
La resolución de enrutamiento sigue una regla estricta de coincidencia. Cuando el binder recibe una interacción, extrae el nombre del comando, subcomando opcional y grupo de subcomandos opcional desde el objeto de interacción de Discord. El registro luego encuentra el handler donde los tres valores coinciden exactamente. Esto significa que un handler raíz (sin sub ni group) nunca recibirá interacciones de subcomandos, y un handler de subcomando nunca recibirá interacciones raíz.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { InfoCommand } from '../commands/info.command';
@SlashCommandHandler(InfoCommand)
export class InfoHandler {
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const ws = interaction.client.ws.ping;
await interaction.reply(`Bot latency: ${ws}ms`);
}
}Inyección de parámetros: @Ctx y decoradores de opción
Los parámetros del método handle los completa SlashInvoker en runtime. @Ctx() inyecta la interacción de Discord en bruto. Los decoradores de opción inyectan el valor resuelto para cada opción nombrada de la interacción. Las posiciones de parámetros son totalmente independientes, por lo que puedes colocar @Ctx en cualquier índice y mezclarlo con decoradores de opción en cualquier orden.
@SlashOption (genérico)
@SlashOption('name') resuelve el valor de opción buscando el tipo de opción en la clase de definición del comando en runtime, y luego llamando al getter correcto de discord.js. Úsalo cuando quieras una inyección rápida sin especificar el tipo en el decorador, porque el tipo se infiere desde los metadatos del comando.
import { Ctx, SlashCommandHandler, SlashOption } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { EchoCommand } from '../commands/echo.command';
@SlashCommandHandler(EchoCommand)
export class EchoHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashOption('message') message: string,
): Promise<void> {
await interaction.reply(message);
}
}Decoradores de parámetros tipados
Nueve decoradores de parámetros tipados incrustan el tipo de opción directamente en metadatos, haciendo que el código del handler sea autoexplicativo. Se omite la búsqueda en la clase de definición del comando, y el dispatcher ya conoce el tipo de opción de Discord desde el propio decorador. Prefiérelos sobre @SlashOption cuando el tipo de opción se conoce al escribir el código.
| Decorador | Tipo de Discord | Valor en runtime |
|---|---|---|
| @SlashStringOption(name) | STRING | string o null |
| @SlashIntegerOption(name) | INTEGER | number o null |
| @SlashNumberOption(name) | NUMBER | number o null |
| @SlashBooleanOption(name) | BOOLEAN | boolean o null |
| @SlashUserOption(name) | USER | User o null |
| @SlashChannelOption(name) | CHANNEL | GuildBasedChannel o null |
| @SlashRoleOption(name) | ROLE | Role o null |
| @SlashMentionableOption(name) | MENTIONABLE | User o Role o null |
| @SlashAttachmentOption(name) | ATTACHMENT | Attachment o null |
import {
Ctx,
SlashCommandHandler,
SlashIntegerOption,
SlashStringOption,
SlashUserOption,
} from '@spraxium/common';
import type { ChatInputCommandInteraction, User } from 'discord.js';
import { BanCommand } from '../commands/ban.command';
@SlashCommandHandler(BanCommand)
export class BanHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashUserOption('target') target: User | null,
@SlashStringOption('reason') reason: string | null,
@SlashIntegerOption('days') days: number | null,
): Promise<void> {
if (!target) {
await interaction.reply({ content: 'No target provided.', flags: 'Ephemeral' });
return;
}
await interaction.reply(`Banned **${target.tag}**, reason: ${reason ?? 'none'}`);
}
}Los nombres de decorador deben coincidir exactamente con los nombres de opción
El string pasado a cualquier decorador de opción debe ser idéntico al nombre
de opción declarado en la definición del comando. Un desajuste hace que el
invoker falle la búsqueda e inyecte null. Verifica bien los nombres de
opción al renombrar opciones de comando, o importa la clase del comando como
referencia de tipo para detectar desajustes durante revisión de código.
@SlashOpt (obsoleto)
@SlashOpt está marcado como @deprecated y se eliminará en una próxima versión mayor. Sigue funcionando de forma idéntica, así que el código existente no se rompe. Migra a @SlashOption o al decorador tipado correspondiente a tu ritmo.
// Before (deprecated)
@SlashOpt('message') message: string | null
// After generic
@SlashOption('message') message: string
// After typed (preferred)
@SlashStringOption('message') message: string | nullInyección de dependencias en handlers
Las clases handler se instancian mediante SpraxiumContainer, por lo que participan completamente en el sistema DI del framework. Puedes declarar parámetros de constructor para cualquier provider registrado en el árbol de módulos. El contenedor DI resuelve dependencias de forma recursiva, por lo que un handler puede depender de servicios que a su vez dependen de otros servicios.
import { Ctx, SlashCommandHandler, SlashStringOption } from '@spraxium/common';
import { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { LocaleCommand } from '../commands/locale.command';
@SlashCommandHandler(LocaleCommand)
export class LocaleHandler {
constructor(private readonly i18n: I18nService) {}
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashStringOption('language') language: string | null,
): Promise<void> {
if (!language || !this.i18n.has(language)) {
const msg = await this.i18n.tUser(
interaction.user.id,
'commands.locale.invalid',
{ available: this.i18n.locales().join(', ') },
);
await interaction.reply({ content: msg, flags: 'Ephemeral' });
return;
}
await this.i18n.setUserLocale(interaction.user.id, language);
const msg = this.i18n.t('commands.locale.set', language, { locale: language });
await interaction.reply({ content: msg, flags: 'Ephemeral' });
}
}Patrones de handler
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { SecretCommand } from '../commands/secret.command';
@SlashCommandHandler(SecretCommand)
export class SecretHandler {
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.reply({
content: 'This message is only visible to you.',
flags: 'Ephemeral',
});
}
}Checklist de diseño para handlers
Usa decoradores de opción tipados
Prefiere @SlashStringOption, @SlashUserOption, etc. sobre el @SlashOption genérico cuando el tipo de opción sea conocido. Los decoradores tipados hacen que el handler sea autoexplicativo y omiten la búsqueda en runtime contra la clase de definición del comando.
Maneja null en parámetros opcionales
Cuando una opción de comando tiene required: false, el parámetro en el
handler será null cuando el usuario omita la opción. Siempre tipa
parámetros opcionales como Type | null y comprueba null antes de usar el
valor.
Responde en tres segundos, o usa @Defer / @AutoDefer
Discord exige una respuesta dentro de tres segundos. Para handlers lentos,
usa @Defer (siempre difiere) o @AutoDefer (difiere solo si se supera el
umbral) en lugar de llamar deferReply() manualmente. Consulta
Respuestas Diferidas.
Mantén las clases handler enfocadas
Cada handler implementa exactamente una operación lógica. Cuando un comando tiene subcomandos, usa clases handler separadas para cada subcomando en vez de ramificar dentro de un único handler.
Usa inyección por constructor para lógica compartida
Cuando varios handlers comparten lógica como validación, formateo o acceso a base de datos, extrae esa lógica a una clase de servicio e inyéctala por el constructor. El contenedor DI gestiona el ciclo de vida.