Por qué la estructura importa desde temprano
Un layout limpio de carpetas mantiene el código de transporte entendible a medida que crece la cantidad de endpoints. Sin límites claros, los archivos de controlador acumulan lógica de negocio, las definiciones DTO quedan dispersas en directorios no relacionados y la ubicación de middleware se vuelve impredecible. Establecer estructura temprano evita que esos problemas se multipliquen cuando el proyecto escala, porque la jerarquía de carpetas comunica por sí sola dónde pertenece cada responsabilidad.
El objetivo es directo: una raíz de ownership en AppModule, ubicación predecible de archivos para cada preocupación de transporte y separación clara entre mapeo request/respuesta (controladores) y lógica de negocio (servicios). Cuando una persona nueva abre el proyecto, el árbol de carpetas debería responder "¿dónde va esto?" sin necesidad de conversación.
Esto importa más en módulos HTTP que en código típico de comandos de Discord porque los endpoints HTTP son un contrato público. Los clientes dependen de estabilidad de paths, formas de respuesta y formatos de error. Un código de transporte desorganizado dificulta auditar qué se expone y facilita introducir cambios incompatibles por accidente.
Estructura recomendada
Rol de cada carpeta
Entender qué controla cada directorio evita filtraciones de responsabilidades entre límites. Los controladores manejan mapeo request/response, los servicios controlan reglas de negocio, los DTOs definen contratos de validación, el middleware aporta comportamiento transversal y los errores modelan escenarios de fallo específicos del dominio.
Responsabilidades enfocadas por archivo
import { HttpController, HttpGet, HttpParam, HttpPatch, HttpBody, HttpStatus } from '@spraxium/http';
import { NotFoundError } from '@spraxium/http';
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) {
const guild = await this.service.getGuildSummary(guildId);
if (!guild) throw new NotFoundError('Guild not found');
return { ok: true, ...guild };
}
@HttpPatch('/:guildId/config')
@HttpStatus(200)
async updateConfig(
@HttpParam('guildId') guildId: string,
@HttpBody(UpdateGuildConfigDto) body: UpdateGuildConfigDto,
) {
return this.service.updateConfig(guildId, body);
}
}Por qué los controladores deben ser delgados
Los controladores son el límite entre transporte HTTP y lógica de aplicación. Su trabajo es leer parámetros de la request, delegar trabajo a servicios y dar forma a la respuesta. Cuando un controlador acumula reglas de negocio, se vuelve difícil de testear porque necesitas un contexto HTTP completo para ejercitar comportamiento de dominio. También se vuelve difícil de reutilizar porque la misma lógica no puede invocarse desde otro punto de entrada como un handler de Signal o una tarea programada.
El patrón es consistente: el controlador llama al servicio, el servicio llama a BotBridge u otras dependencias y los errores suben por la jerarquía HttpError hasta que el handler centralizado onError los normaliza. Esta separación por capas mantiene cada archivo enfocado y cada prueba aislada.
Los servicios se resuelven por RouteRegistry usando orden topológico, lo que significa que pueden depender entre sí siempre que no existan referencias circulares. Si se detecta una dependencia circular, el startup falla de inmediato con un mensaje de error claro que lista las clases involucradas.
Escalar hacia bounded contexts
Cuando la superficie API crece más allá de unos pocos controladores, las carpetas planas generan ruido. En ese punto, divide src/http en subdirectorios por dominio. Cada subdirectorio obtiene sus propios controladores, servicios, DTOs y errores, mientras todo sigue registrándose mediante los mismos metadatos de @HttpClientModule en AppModule.
Pasos prácticos de evolución
Comienza con una carpeta única de transporte
Empieza con src/http y subcarpetas de controladores, servicios y DTO incluso con una superficie API pequeña. Esto da a tu equipo un mapa mental estable antes de que el proyecto escale. Evita colocar archivos HTTP directamente en src/ porque se mezclarán con código de comandos de Discord.
Promueve lógica compartida a servicios
Mantén delgados los controladores moviendo reglas de negocio no triviales a clases de servicio. Los handlers de ruta deberían coordinar request y response, no controlar comportamiento de dominio. Los servicios también se inyectan con BotBridge, lo que mantiene la interacción con Discord detrás de una abstracción limpia.
Crea clases de error personalizadas desde temprano
Define subclases de HttpError específicas de dominio en una carpeta errors/ en lugar de lanzar errores genéricos con mensajes string. Los errores con nombre son más fáciles de localizar en logs, testear en aserciones y filtrar en dashboards de monitoreo.
Separa por bounded context cuando haga falta
A medida que crezca el volumen de endpoints, divide src/http en carpetas de dominio como guilds/, moderation/ y members/. Mantén app.module.ts como raíz única de composición para todos los contextos. Cada carpeta de dominio controla sus controladores, servicios y DTOs.
Protege los límites de ownership
Sigue usando una sola entrada defineHttp y una sola fuente de metadatos AppModule incluso tras refactors. Múltiples raíces de metadatos vuelven difícil razonar sobre ciclo de vida y política de seguridad. Todos los controladores de todos los dominios se listan en el mismo arreglo controllers de HttpClientModule.