Por qué existe el bridge
BotBridge es la capa de abstracción entre controladores HTTP y operaciones de Discord. Los controladores deben depender de métodos BotBridge, no de internals crudos de discord.js como client.guilds.cache o guild.members.ban(). Esto mantiene estable el comportamiento de rutas incluso cuando tu topología de despliegue cambia de proceso único a workers sharded, porque el código de controladores no cambia.
Sin el bridge, pasar de modo directo a modo sharded requeriría reescribir cada controlador que interactúa con Discord. El patrón bridge aísla esa diferencia en dos clases concretas, DirectBotBridge y ShardedBotBridge, ambas implementando el mismo contrato abstracto. BridgeFactory selecciona la implementación correcta en startup del servidor y la inyecta en el mapa de dependencias DI, de modo que controladores y servicios pueden declarar BotBridge como parámetro de constructor y recibir automáticamente la implementación adecuada.
El bridge también fuerza serialización en el límite. Los objetos de Discord.js contienen referencias circulares, cachés vivas y métodos que no son seguros para serializar a JSON. El bridge devuelve objetos planos tipados como SerializedGuild, SerializedMember y SerializedBan que sí son seguros para transporte HTTP.
Selección de modo runtime
BridgeFactory.create(client, manager) selecciona la implementación bridge según cuál referencia runtime esté disponible. La factoría sigue una precedencia estricta: ShardingManager tiene prioridad sobre Client. Si no se provee ninguno, la factoría lanza error en startup, evitando que el servidor corra sin conectividad de Discord.
| Modo | Implementación | Cómo accede a Discord | Cuándo usarlo |
|---|---|---|---|
| Directo | DirectBotBridge | Usa la instancia local de Client de Discord. Lee desde caché de guilds y llama métodos API directamente en el mismo proceso. | Bots de proceso único o entornos de desarrollo donde sharding no es necesario. |
| Sharded | ShardedBotBridge | Usa | Bots en producción con múltiples shards donde el servidor HTTP corre en el proceso padre sin un Client local. |
Contrato de capacidades BotBridge
La clase abstracta BotBridge define 12 métodos organizados en tres grupos de operación. Cada implementación concreta debe cumplir los 13 métodos, garantizando que los controladores dispongan de una API completa y consistente sin importar el modo runtime.
Los métodos de lectura retornan objetos serializados o null. Los métodos de acción retornan flags booleanos de éxito. Cada método bridge envuelve su cuerpo en try/catch: ante fallo, los métodos de lectura retornan null (o arreglo vacío para getBans) y los de acción retornan false. Esto significa que los controladores nunca necesitan manejar excepciones inesperadas provenientes de llamadas bridge.
Cómo funciona el bridge sharded
ShardedBotBridge usa manager.broadcastEval() para ejecutar un closure en cada shard del clúster. El closure recibe el Client local del shard y un objeto de contexto serializado con parámetros (guild ID, user ID, etc). Solo el shard que posee el guild objetivo lo encontrará en su caché, así que los demás retornan null o false.
Para operaciones de lectura, el bridge recolecta resultados de todos los shards y devuelve el primer valor no-null usando .find(r => r !== null). Para operaciones de acción, retorna true si cualquier shard devolvió true usando .some(r => r === true). Este patrón fan-out permite que cada operación alcance al shard correcto sin que el servidor HTTP necesite conocer topología de shards ni mapeo guild-a-shard.
El trade-off es latencia: cada llamada bridge en modo sharded paga el costo de comunicación entre procesos hacia todos los shards. En bots con muchos shards, considera cachear mapeos guild-a-shard si endpoints con alta lectura se vuelven un cuello de botella.
Controlador consumiendo el bridge
import { HttpController, HttpGet, HttpPost, HttpParam, HttpBody, HttpStatus } from '@spraxium/http';
import { BotBridge, NotFoundError } from '@spraxium/http';
import { BanMemberDto } from '../dto/ban-member.dto';
@HttpController('/guilds/:guildId/moderation')
export class ModerationHttpController {
constructor(private readonly bridge: BotBridge) {}
@HttpGet('/bans')
async listBans(@HttpParam('guildId') guildId: string) {
const bans = await this.bridge.getBans(guildId);
return { ok: true, guildId, bans, count: bans.length };
}
@HttpGet('/members/:userId')
async getMember(
@HttpParam('guildId') guildId: string,
@HttpParam('userId') userId: string,
) {
const member = await this.bridge.getMember(guildId, userId);
if (!member) throw new NotFoundError('Member not found in guild');
return { ok: true, member };
}
@HttpPost('/bans/:userId')
@HttpStatus(201)
async banMember(
@HttpParam('guildId') guildId: string,
@HttpParam('userId') userId: string,
@HttpBody(BanMemberDto) body: BanMemberDto,
) {
const success = await this.bridge.banMember(
guildId,
userId,
body.reason,
{ deleteMessageSeconds: body.deleteMessageSeconds },
);
return { ok: success, guildId, userId };
}
}Modelo de serialización
El bridge usa tres clases serializer para convertir objetos runtime de discord.js en payloads planos tipados seguros para transporte JSON. Estos serializers eliminan referencias circulares, convierten fechas a strings ISO y extraen solo campos que clientes externos necesitan. Los tipos serializados sirven como límite estable de API: los controladores devuelven estos tipos y los clientes los consumen.
| Serializer | Entrada | Campos de salida |
|---|---|---|
| GuildSerializer | discord.js Guild | id, name, icon, memberCount, ownerId, createdAt (string ISO), features (arreglo de strings). |
| MemberSerializer | discord.js GuildMember | id, username, displayName, discriminator, avatar, bot, joinedAt, createdAt, roles (arreglo de SerializedRole con id/name/color/position/permissions/managed/mentionable), pending, communicationDisabledUntil. |
| BanSerializer | discord.js GuildBan | user (anidado SerializedUser con id/username/discriminator/avatar/bot/createdAt), reason (string o null). |
En modo directo, los serializers se ejecutan en el mismo proceso que el cliente Discord. En modo sharded, el closure de broadcastEval realiza serialización inline en el shard dueño del guild, retornando objetos planos que cruzan de forma segura el límite IPC. Ambas rutas producen la misma forma de salida, así que el código de controlador es idéntico sin importar el modo.
Checklist de diseño para endpoints bridge
Diseña para transparencia de modo
Mantén lógica de controladores idéntica en despliegues directos y sharded dependiendo solo de métodos BotBridge. La selección de modo debe permanecer como preocupación de configuración en defineHttp({ sharding }), no de controladores. Nunca importes DirectBotBridge ni ShardedBotBridge directamente en código de controlador.
Devuelve solo payloads serializados
Trata los tipos de salida serializer como tu límite de API. Nunca devuelvas objetos runtime de discord.js desde handlers porque contienen referencias circulares y cachés vivas que rompen serialización JSON. Si necesitas campos no cubiertos por serializers por defecto, crea un serializer personalizado en lugar de exponer objetos crudos de discord.js.
Maneja lecturas nullable de forma explícita
Operaciones de lectura pueden retornar null cuando entidades faltan o están no disponibles en el estado runtime actual. Convierte resultados null en respuestas HTTP claras según el contrato de tu ruta. Un guild null típicamente se mapea a NotFoundError(404), mientras un miembro null puede justificar NotFoundError o respuesta vacía filtrada según semántica del endpoint.
Mantén mutaciones idempotentes cuando sea posible
Los métodos de acción retornan booleanos y pueden ejecutarse en contextos distribuidos donde hay reintentos. La semántica del endpoint debería tolerar reintentos sin efectos secundarios. Por ejemplo, banear a un usuario ya baneado retorna false pero no crea conflicto, y el controlador aún puede reportar resultado de operación sin lanzar error.
Valida payloads de mutación con DTOs
Usa @HttpBody(DtoClass) con constraints de class-validator en cada endpoint de mutación. Acciones bridge como banMember y timeoutMember aceptan parámetros que deberían validarse antes de llegar a la capa bridge. La validación DTO detecta entradas malformadas temprano y produce respuestas de error estructuradas automáticamente.
Los métodos bridge nunca lanzan excepciones en fallos
Todos los métodos bridge envuelven internamente operaciones en try/catch. Métodos de lectura retornan null en fallo, y métodos de acción retornan false. Diseña el manejo de errores de tu controlador alrededor de estos valores de retorno en lugar de esperar excepciones lanzadas por llamadas bridge.