Por que a estrutura importa desde cedo
Uma organização limpa de pastas mantém o código de transporte compreensível à medida que o número de endpoints cresce. Sem fronteiras claras, arquivos de controller acumulam regra de negócio, definições de DTO acabam espalhadas por diretórios sem relação e o lugar do middleware vira algo imprevisível. Definir a estrutura cedo impede que esses problemas se acumulem conforme o projeto escala, porque a própria hierarquia de pastas comunica onde cada responsabilidade deve ficar.
O objetivo é direto: uma única raiz de controle no AppModule, posicionamento previsível de arquivos para cada responsabilidade de transporte e separação clara entre mapeamento de requisição, nos controllers, e regra de negócio, nos services. Quando alguém novo abrir o projeto, a árvore de pastas deve responder "onde isso entra?" sem exigir conversa nenhuma.
Isso importa ainda mais em módulos HTTP do que em código típico de comandos do Discord porque endpoints HTTP são um contrato público. Clientes dependem da estabilidade dos caminhos, dos formatos de resposta e dos formatos de erro. Código de transporte desorganizado dificulta auditar o que está exposto e facilita introduzir breaking changes sem perceber.
Estrutura recomendada
Papel de cada pasta
Entender o que cada diretório controla evita vazamento de responsabilidades entre fronteiras. Controllers lidam com mapeamento de requisição e resposta, services controlam as regras de negócio, DTOs definem contratos de validação, middleware entrega comportamento transversal e errors modelam cenários de falha específicos do domínio.
Responsabilidades focadas por arquivo
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 que controllers devem continuar finos
Controllers são a fronteira entre o transporte HTTP e a lógica da aplicação. O trabalho deles é ler parâmetros da requisição, delegar o trabalho aos services e modelar a resposta. Quando controllers acumulam regras de negócio, ficam difíceis de testar porque você passa a depender de um contexto HTTP completo para exercitar comportamento de domínio. Eles também ficam difíceis de reaproveitar, porque a mesma lógica não pode ser chamada facilmente a partir de outro ponto de entrada, como um handler de Signal ou uma tarefa agendada.
O padrão é consistente: o controller chama o service, o service chama BotBridge ou outras dependências, e os erros sobem pela hierarquia de HttpError até que o handler centralizado onError os normalize. Esse encadeamento mantém cada arquivo focado e cada teste isolado.
Services são resolvidos por RouteRegistry usando ordenação topológica, o que significa que eles podem depender uns dos outros desde que não haja referências circulares. Se uma dependência circular for detectada, a inicialização falha imediatamente com uma mensagem clara listando as classes envolvidas.
Escalando para contextos delimitados
Quando a superfície da API cresce além de alguns controllers, pastas planas começam a fazer barulho demais. Nesse ponto, divida src/http em subdiretórios por domínio. Cada subdiretório ganha seus próprios controllers, services, DTOs e errors, mas tudo continua sendo registrado pelos mesmos metadados de @HttpClientModule no AppModule.
Passos práticos de evolução
Comece com uma pasta única de transporte
Comece com src/http contendo pastas de controllers, services e DTOs mesmo
para uma API pequena. Isso dá ao time um mapa mental estável antes de o
projeto crescer. Evite colocar arquivos HTTP diretamente em src/, porque
eles vão se misturar com o código de comandos do Discord.
Promova a lógica compartilhada para services
Mantenha os controllers finos movendo regras de negócio não triviais para classes de service. Handlers de rota devem coordenar requisição e resposta, não controlar comportamento de domínio. Services também recebem BotBridge por injeção, o que mantém a interação com o Discord atrás de uma abstração limpa.
Crie classes de erro customizadas cedo
Defina subclasses de HttpError específicas do domínio em uma pasta
errors/, em vez de lançar erros genéricos com mensagens em string. Erros
nomeados são mais fáceis de achar em logs, testar em assertions e filtrar em
dashboards de monitoramento.
Divida por contexto delimitado quando necessário
Conforme o volume de endpoints cresce, divida src/http em pastas de
domínio como guilds/, moderation/ e members/. Mantenha
app.module.ts como a composição raiz única para todos os contextos. Cada
pasta de domínio controla seus controllers, services e DTOs.
Proteja as fronteiras de controle
Continue usando uma entrada de defineHttp e uma fonte de metadados no
AppModule mesmo depois de refactors. Múltiplas raízes de metadados tornam o
ciclo de vida e a política de segurança difíceis de raciocinar. Todos os
controllers de todos os domínios devem aparecer no mesmo array controllers
de HttpClientModule.