Spraxium logoSpraxium

Arquitetura de Pastas HTTP

Organize arquivos HTTP com fronteiras de transporte claras e caminhos de crescimento fáceis de manter. Mantenha AppModule como dono único dos metadados enquanto separa controllers, services, DTOs e middleware.

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

Arquitetura HTTP sugerida
app.module.ts
main.ts
guild-http.controller.ts
moderation-http.controller.ts
guild-http.service.ts
moderation-http.service.ts
admin.guard.ts
update-guild-config.dto.ts
ban-member.dto.ts
request-audit.middleware.ts
guild-locked.error.ts
http.config.ts
spraxium.config.ts

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.

Arquitetura HTTP escalada com fronteiras de domínio
app.module.ts# Dono único dos metadados para todos os domínios HTTP
guild-http.controller.ts
guild-http.service.ts
update-guild-config.dto.ts
moderation-http.controller.ts
moderation-http.service.ts
ban-member.dto.ts
member-http.controller.ts
member-http.service.ts
request-audit.middleware.ts# Middleware compartilhado entre domínios
guild-locked.error.ts# Erros customizados compartilhados entre domínios

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.