Listeners como límites de evento
Un listener debe ser el límite más delgado posible entre un evento de Discord y tu lógica de dominio. Su trabajo es recibir el payload del evento, aplicar filtrado básico como ignorar cuentas de bots y luego llamar al servicio apropiado para manejar la lógica de negocio real. Cuando mantienes listeners delgados, tu código de dominio sigue siendo testeable sin mockear el sistema de eventos de Discord, y tus listeners se mantienen legibles porque cada uno hace muy poco por sí solo.
El enfoque opuesto, poner la lógica de negocio directamente dentro de métodos listener, funciona en bots pequeños pero se degrada rápidamente a medida que crece el código. Si tu algoritmo de ranking de leaderboard vive dentro de un MessageCreateListener, no puedes testear ese algoritmo sin simular mensajes de Discord. Si ese mismo algoritmo necesita ejecutarse más tarde desde un endpoint de API o una tarea programada, de todos modos tendrás que extraerlo. Empezar desde el principio con el patrón de delegación a servicios evita por completo esa refactorización.
Este patrón también deja claro dónde mirar cuando algo falla. Si el evento no se dispara en absoluto, el problema está en el enlace del listener o en los gateway intents. Si el evento se dispara pero ocurre lo incorrecto, el problema está en el servicio. La separación acelera la depuración porque cada capa tiene una responsabilidad clara y acotada.
Ejemplo de delegación a servicio
Aquí está el patrón en práctica. El listener recibe el evento MessageCreate, verifica si el autor es un bot y luego delega en un LeaderboardService que maneja la lógica real de tracking. El listener no sabe cómo funciona el puntaje, y el servicio no sabe que está siendo llamado desde un evento de Discord.
import { Events, Listener, On } from '@spraxium/common';
import { Logger } from '@spraxium/core';
import type { Message } from 'discord.js';
import { LeaderboardService } from '../../services/leaderboard.service';
@Listener(Events.MessageCreate)
export class MessageCreateListener {
private readonly logger = new Logger(MessageCreateListener.name);
constructor(private readonly leaderboard: LeaderboardService) {}
@On()
async onMessage(message: Message): Promise<void> {
if (message.author.bot) return;
this.logger.debug(`Message from ${message.author.id} in guild ${message.guildId}`);
await this.leaderboard.trackMessage(message.author.id, message.guildId);
}
}El servicio es una clase inyectable regular que el módulo registra en su arreglo providers. El listener lo recibe mediante inyección por constructor y llama sus métodos. Si más adelante necesitas la misma lógica de puntaje desde otro punto de entrada, inyectas el mismo servicio ahí sin duplicar código.
Organización de módulos
Un módulo de eventos bien organizado agrupa listeners relacionados y mantiene el archivo de declaración del módulo en la raíz de la carpeta. A medida que tu bot maneja más eventos, esta estructura escala sin volverse difícil de navegar. Cada archivo listener maneja un evento, y el archivo del módulo es el índice que declara qué está activo.
El módulo une todo al listar todos los listeners y providers en un solo lugar. Cualquiera que lea el archivo del módulo puede ver de inmediato qué eventos maneja el módulo y qué servicios están disponibles para inyección.
import { Module } from '@spraxium/common';
import { LeaderboardService } from '../services/leaderboard.service';
import { WelcomeService } from '../services/welcome.service';
import { MemberJoinListener } from './listeners/member-join.listener';
import { MemberLeaveListener } from './listeners/member-leave.listener';
import { MessageCreateListener } from './listeners/message-create.listener';
import { ReadyListener } from './listeners/ready.listener';
@Module({
providers: [LeaderboardService, WelcomeService],
listeners: [
ReadyListener,
MessageCreateListener,
MemberJoinListener,
MemberLeaveListener,
],
})
export class EventsModule {}Patrones prácticos para métodos listener
Los métodos listener deben seguir una estructura consistente que los vuelva predecibles en todo tu código. Empieza con retornos tempranos para eventos irrelevantes, luego haz cualquier transformación liviana y, por último, llama al servicio. Este patrón de tres pasos mantiene cada método listener legible y fácil de revisar.
Filtra primero los eventos irrelevantes
Empieza el método con guard clauses que retornen temprano para eventos que no te interesan. Verificar si el autor es un bot, si el mensaje viene de un canal específico o si la interacción tiene el tipo correcto debe ocurrir antes de cualquier trabajo real. Esto evita llamadas de servicio innecesarias y mantiene alto el rendimiento de eventos.
Extrae lo que el servicio necesita
Extrae del payload del evento los datos específicos que espera tu servicio. En lugar de pasar el objeto completo de mensaje o miembro de Discord.js a tu servicio, extrae el user ID, guild ID, channel ID o cualquier campo concreto que el servicio necesite. Esto mantiene tu capa de servicios desacoplada de tipos de Discord y facilita testear con datos mock simples.
Delega en el servicio inyectado
Llama al método del servicio con los datos extraídos. El trabajo del listener termina en ese punto. Cualquier error lanzado por el servicio será capturado por el pipeline de excepciones para eventos compatibles, así que no necesitas bloques try-catch en el propio método listener.
Evita trabajo pesado en el constructor
No hagas operaciones costosas como conexiones a base de datos, requests HTTP o lecturas de archivos en el constructor del listener. El constructor se ejecuta durante el bootstrap antes de que el bot se conecte a Discord. Mueve la lógica de inicialización a la primera llamada del handler o a un lifecycle hook si el framework lo soporta.
Página siguiente
Continúa con Excepciones e integración de guards.