Estruturas de Projeto

Boas Práticas de Estrutura de Pastas

A estrutura de diretórios de um projeto não é apenas organização — é documentação viva da arquitetura. Uma estrutura bem pensada comunica onde cada coisa vive, quais são os limites do sistema e quais convenções o time acordou seguir. Uma estrutura mal pensada esconde a arquitetura, cria fricção e vira lixeira.

Intenção

Definir convenções de diretório que tornam a arquitetura do sistema visível na estrutura de arquivos, facilitando a navegação, reduzindo o tempo para encontrar onde algo está ou deve estar, e deixando claro quais são os limites e responsabilidades de cada parte do sistema.

Uma boa estrutura de pastas responde a três perguntas sem que o desenvolvedor precise abrir nenhum arquivo: o que este projeto faz, como ele é organizado e onde eu coloco o código novo.

Problema

Sem convenções explícitas, projetos crescem de forma entrópica:

  • Cada desenvolvedor coloca arquivos onde acha conveniente.
  • Código de naturezas diferentes (configuração, lógica de negócio, utilitários, infraestrutura) mistura-se nos mesmos diretórios.
  • A pasta utils/ vira repositório de tudo que não tem dono claro.
  • Novos membros do time levam semanas para entender onde procurar algo.
  • A arquitetura real e a estrutura de pastas divergem — o código diz uma coisa, a pasta diz outra.

O custo não é apenas estético. Estrutura confusa gera imports circulares, torna refatorações arriscadas e dificulta a visibilidade do que pode ser reutilizado.

Estrutura

Convenções comuns de nível de projeto

Independente da linguagem ou framework, certas convenções de diretório de nível raiz se repetem na maioria dos projetos bem organizados:

  projeto/
  ├── src/           ← código-fonte da aplicação (único lugar com lógica)
  ├── tests/         ← testes automatizados (ou __tests__/ junto ao src/)
  │     ├── unit/
  │     ├── integration/
  │     └── e2e/
  ├── docs/          ← documentação técnica (ADRs, diagramas, guias)
  ├── config/        ← arquivos de configuração de ambiente e ferramentas
  │     ├── jest.config.ts
  │     └── tsconfig.json
  ├── scripts/       ← scripts de build, seed, migração e automação
  └── dist/          ← artefato compilado (gerado; nunca versionado)

Exemplo: backend API (Node/Express com camadas)

  src/
  ├── users/                       ← feature: usuários
  │     ├── http/
  │     │     ├── UserController.ts
  │     │     └── CreateUserDto.ts
  │     ├── application/
  │     │     └── CreateUserUseCase.ts
  │     ├── domain/
  │     │     ├── User.ts           ← entidade de domínio
  │     │     └── IUserRepository.ts ← interface (porta)
  │     └── infra/
  │           └── PrismaUserRepository.ts ← implementação concreta
  │
  ├── orders/                      ← feature: pedidos
  │     ├── http/
  │     ├── application/
  │     ├── domain/
  │     └── infra/
  │
  ├── shared/                      ← código genuinamente compartilhado
  │     ├── errors/                ← classes de erro de domínio
  │     ├── types/                 ← tipos TypeScript compartilhados
  │     └── middleware/            ← middlewares HTTP transversais
  │
  └── app.ts                       ← bootstrap: monta o servidor Express

Exemplo: frontend SPA (React/Vue)

  src/
  ├── features/                    ← funcionalidades da aplicação
  │     ├── auth/
  │     │     ├── components/      ← componentes específicos de auth
  │     │     ├── hooks/           ← hooks de auth (useAuth, useLogin)
  │     │     ├── services/        ← chamadas de API de auth
  │     │     └── types.ts         ← tipos de auth
  │     │
  │     └── dashboard/
  │           ├── components/
  │           ├── hooks/
  │           └── services/
  │
  ├── components/                  ← componentes de UI reutilizáveis (sem lógica de negócio)
  │     ├── Button/
  │     │     ├── Button.tsx
  │     │     └── Button.test.tsx
  │     └── Modal/
  │
  ├── hooks/                       ← hooks genéricos (useDebounce, useFetch)
  ├── services/                    ← cliente HTTP, configuração de API
  ├── store/                       ← estado global (Redux, Zustand, Pinia)
  ├── types/                       ← tipos TypeScript globais
  └── main.tsx                     ← entry point

Como a estrutura expõe (ou esconde) a arquitetura

Screaming Architecture

Robert C. Martin propôs que a estrutura de um projeto deveria "gritar" o que o sistema faz — não o framework que ele usa. Uma pasta raiz chamada rails/ grita "este é um projeto Rails". Uma pasta raiz com users/, orders/ e payments/ grita "este é um sistema de e-commerce". A segunda revela a intenção do negócio.

Na prática, isso significa que os diretórios de nível mais alto devem refletir as funcionalidades ou domínios do sistema, não os mecanismos técnicos. Frameworks e bibliotecas são detalhes — deveriam aparecer apenas nas camadas mais externas.

Onde colocar os testes

Há duas convenções, cada uma com trade-offs:

  • Junto ao código (User.test.ts ao lado de User.ts): facilita encontrar e manter o teste ao lado da implementação. Favorece testes unitários. Mais comum em projetos JavaScript/TypeScript com Jest ou Vitest.
  • Diretório tests/ separado: separa claramente o código de produção do código de teste. Facilita ter testes de integração e e2e que testam múltiplos módulos. Mais comum em projetos Python, Go e Java.

O mais importante é ser consistente. Misturar as duas convenções no mesmo projeto cria confusão sobre onde o teste de algo está.

Princípios práticos

Nomeie diretórios pelo propósito, não pelo tipo

helpers/, utils/ e misc/ descrevem o tipo de arquivo (código auxiliar), não o propósito. Substitua por nomes que comuniquem a intenção: formatters/, validators/, date-utils/. Se não consegue nomear o diretório com precisão, provavelmente o código dentro dele não tem coesão suficiente para estar junto.

Profundidade máxima útil

Estruturas com mais de quatro ou cinco níveis de profundidade são difíceis de navegar. Se o caminho de um arquivo é src/features/orders/services/handlers/processors/, é provável que a hierarquia esteja modelando subdivisões que poderiam ser capturadas pelo nome do arquivo em vez de por diretórios.

Consistência sobre perfeição

Uma estrutura boa aplicada de forma consistente é mais valiosa do que uma estrutura "perfeita" onde cada desenvolvedor interpreta de forma diferente onde as coisas vão. Documente as convenções no README ou num ADR (Architecture Decision Record) e siga-as.

Boas práticas vs anti-padrões

Boas práticas

  • Separar código-fonte (src/) de artefatos gerados (dist/) e de scripts de automação (scripts/).
  • Nomes de diretório em minúsculas e com hífen (kebab-case) — evitam problemas em sistemas de arquivo case-insensitive.
  • Estrutura que reflete o domínio do negócio nos níveis superiores.
  • Testes próximos ao código que testam, ou num diretório de testes com estrutura espelhada.
  • Um index.ts (barrel) por diretório apenas quando a API pública do módulo é estável e explicitamente definida.

Anti-padrões

  • utils/ sem critério: acaba com arquivos de formatação de data, helpers de string, funções matemáticas e validadores de CPF no mesmo diretório.
  • shared/ ou common/ sem critério de admissão: vira o novo utils/.
  • Diretórios com um único arquivo: se há apenas um arquivo, o diretório provavelmente não agrega valor.
  • Estrutura que copia o framework, não o domínio: pastas controllers/, models/, views/ como nível mais alto ocultam o que o sistema faz.
  • Barrel re-exports encadeados (veja abaixo).

Armadilhas comuns

1. A pasta utils/ como lixeira

utils/ começa com uma função de formatação de data e termina com 40 arquivos de naturezas completamente distintas. O problema não é ter código utilitário — é agrupá-lo sem critério de coesão. Quando você não consegue descrever o que utils/ contém em uma frase, ela virou lixeira.

Solução: divida por domínio de responsabilidade. formatters/ para formatação de valores, validators/ para validações, parsers/ para parsing. Se um utilitário pertence semanticamente a uma feature, coloque-o dentro dessa feature.

2. Barrel re-exports criando acoplamento circular

Barrel exports (index.ts que re-exporta tudo do diretório) são convenientes para importar de um módulo por um caminho curto. O problema surge quando barrels são encadeados: A exporta B e C; B importa de A para usar C. Resultado: dependência circular que o bundler pode resolver de formas imprevisíveis.

  // anti-padrão: B importa de A para usar C (que A re-exporta)
  // src/users/index.ts
  export { UserService } from './UserService';
  export { UserUtils } from './UserUtils';   // C

  // src/users/UserService.ts
  import { UserUtils } from '../users';      // importa A para chegar em C
  //                           ^^^^^^ cria dependência circular em UserService -> users/index -> UserService

  // correto: importar diretamente
  import { UserUtils } from './UserUtils';

Use barrels com moderação. Um index.ts é adequado para definir a API pública de um módulo — o que ele expõe para outros módulos. Não use barrels como atalho para evitar imports diretos dentro do mesmo módulo.

3. Diretório shared/ sem critério de entrada

shared/ resolve o problema de código usado por múltiplas features, mas sem um critério claro de o que pertence a ele, torna-se outro depósito. Dois sinais de que algo não pertence a shared/: (1) só uma feature usa; (2) o código sabe de detalhes de uma feature específica. Nenhum dos dois casos é "genuinamente compartilhado".

4. Estrutura que diverge da arquitetura real

Uma estrutura por camadas com pastas controllers/ e services/ é cosmética se o UserController acessa o banco diretamente. Uma estrutura por features é enganosa se todas as features importam livremente umas das outras. A estrutura de pastas deve refletir os limites reais do sistema — não criá-los no papel enquanto o código os viola.

Tópicos relacionados

A estrutura de pastas e a organização por feature vs camada são decisões complementares: a organização define o critério primário de agrupamento; a estrutura de pastas define as convenções de nomeação, profundidade e localização de cada tipo de artefato dentro desse critério.

A Arquitetura em Camadas define regras de dependência entre partes do sistema. A estrutura de pastas é a forma como essas camadas se tornam visíveis no sistema de arquivos. Uma boa estrutura torna as violações de arquitetura óbvias — um import de infra/ para domain/ é imediatamente suspeito quando os diretórios estão nomeados de forma consistente com a arquitetura.