Descrição da Estrutura de Pastas com princípios SOLID

Descrição da Estrutura de Pastas com princípios SOLID

/src

  • Diretório raiz contendo todo o código-fonte da aplicação.

    /server.ts

    • Arquivo na raiz responsável apenas por iniciar o servidor. Ele importa o módulo app de ./app para configurar o servidor e também acessa as variáveis de ambiente a partir do módulo ./env.

    /app.ts

    • Arquivo responsável por configurar o servidor Fastify, registrar as rotas e middlewares, e também por configurar o tratamento de erros.

    /@types

    • Armazena declarações de tipos personalizados ou extensões de tipos existentes.

    /docs

    • Documentação do projeto, incluindo especificações da API e outras informações relevantes.

    /env

    • Configurações específicas de ambiente.

    /http

    • Código relacionado à camada HTTP da aplicação.

      /controllers
      • Controladores que lidam com a lógica de apresentação e delegam a lógica de negócios para os use-cases.

        /[entidade]/routes.ts

        • Arquivo responsável por definir as rotas da entidade específica. Importa e utiliza os middlewares e controllers associados a essas rotas.

        /[entidade]/[ação].ts

        • Arquivos que implementam a lógica específica para cada ação da entidade. Utiliza uma factory para executar o repositório correspondente.

        /[entidade]/[ação].spec.ts

        • Arquivos de teste E2E (End-to-End) para os endpoints. Cada teste cria e destrói um banco de dados para garantir o isolamento.
      /middlewares
      • Middlewares que fazem o pré-processamento de requisições e o pós-processamento de respostas.

    /lib

    • Bibliotecas e utilitários que não pertencem a uma camada específica da aplicação, como por exemplo, bibliotecas para manipulação de datas ou strings.

    /repositories

    • Abstrações para acesso ao banco de dados.
      /in-memory
      • Repositórios que usam armazenamento em memória (útil para testes ou desenvolvimento).

        [in-memory-entidadeName.ts] (ex: in-memory-users-repository.ts)

      /supabase
      • Repositórios que interagem com o Supabase como banco de dados.
        /[supabase-entidadeName-repositorie.ts]
        • Arquivo dedicado para realizar consultas ao banco de dados Supabase.

    /use-cases

    • Casos de uso da aplicação, ou seja, lógica de negócios.

      /errors

      • [error].ts (ex: invalid-credentials-error.ts)
      • Arquivos dedicados a representar erros específicos que podem ser lançados pelos casos de uso e capturados pelos middlewares.

      /factories

      • make-[entidade]-use-case.ts (ex: make-register-use-case.ts)
      • Fábricas responsáveis por instanciar os casos de uso e suas dependências, como repositórios.

      [nome_da_entidade].ts (ex: register.ts)

      • Arquivos de casos de uso que implementam as regras de negócio e recebem o repositório como uma dependência.

      [nome_da_entidade].spec.ts (ex: register.spec.ts)

      • Arquivos de teste unitário para os casos de uso, usando um repositório "in-memory" para agilizar os testes.

    /utils

    • Utilitários e funções auxiliares que podem ser usadas em diferentes partes da aplicação.

    /validators

    • Validadores de entrada para as rotas e outras formas de dados.

Passo a Passo com SOLID - Arquivo por Arquivo

1. server.ts

  • Single Responsibility Principle (SRP): Este arquivo tem a única responsabilidade de iniciar o servidor.

2. app.ts

  • Single Responsibility Principle (SRP): Este arquivo é responsável por configurar o servidor Fastify e seus recursos, sem envolver lógica de negócios ou acesso a dados.

  • Open/Closed Principle (OCP): O arquivo está aberto para extensão, você pode adicionar mais rotas, middlewares ou configurações de plugins sem modificar o código existente.

  • Dependency Inversion Principle (DIP): As dependências, como rotas e plugins, são injetadas através do método register, tornando o código mais modular e fácil de testar.

3. routes.ts

  • Single Responsibility Principle (SRP): O arquivo tem a única responsabilidade de definir as rotas associadas a uma entidade específica.

  • Open/Closed Principle (OCP): O arquivo está aberto para extensão, já que novas rotas ou middlewares podem ser adicionadas facilmente.

  • Dependency Inversion Principle (DIP): A função usersRoutes recebe um objeto FastifyInstance como argumento, tornando o código mais modular e fácil de testar.

4. [ação].ts (por exemplo, register.ts)

  • Single Responsibility Principle (SRP): Cada arquivo [ação].ts tem a responsabilidade única de implementar a lógica para uma ação específica da entidade.

  • Open/Closed Principle (OCP): Extensível para novas regras ou lógicas de negócios sem a necessidade de modificar o código existente.

  • Dependency Inversion Principle (DIP): Utiliza uma factory para executar a ação correspondente no repositório, tornando o código mais modular e testável.

  • Interface Segregation Principle (ISP): Cada ação tem seu próprio arquivo, tornando as interfaces mais específicas e orientadas para o cliente.

5. [supabase-entidadeName-repositorie.ts]

  • Single Responsibility Principle (SRP): Este arquivo tem a única responsabilidade de executar as consultas ao banco de dados Supabase.

  • Open/Closed Principle (OCP): Extensível para novas consultas ou lógicas de acesso aos dados sem modificar o código existente.

  • Dependency Inversion Principle (DIP): As consultas são abstraídas em métodos específicos, tornando o código mais modular e testável.

6. nome_da_entidade.ts (ex: register.ts)

  • Single Responsibility Principle (SRP): Implementa as regras de negócio para uma funcionalidade específica.

  • Open/Closed Principle (OCP): Extensível para novas regras de negócio sem a necessidade de modificar o código existente.

  • Dependency Inversion Principle (DIP): As regras de negócio são desacopladas do repositório. Se o repositório mudar no futuro, as regras de negócio não precisam ser alteradas.

  • Liskov Substitution Principle (LSP): O caso de uso espera uma abstração (um repositório) e é imutável em relação aos detalhes, permitindo a substituição do repositório sem afetar o caso de uso.

7. [nome_da_entidade].spec.ts (ex: register.spec.ts)

  • Single Responsibility Principle (SRP): Testa uma única funcionalidade ou caso de uso.

  • Open/Closed Principle (OCP): Novos testes podem ser adicionados sem alterar os testes existentes.

  • Liskov Substitution Principle (LSP): Utiliza um repositório "in-memory", permitindo que o repositório real seja substituído nos testes sem afetar a lógica do caso de uso.

  • Dependency Inversion Principle (DIP): O caso de uso é testado isoladamente, com o repositório sendo uma dependência injetada.

8. [ação].spec.ts (ex: register.spec.ts)

  • Single Responsibility Principle (SRP): Cada teste E2E foca em testar uma única rota ou fluxo de interação do usuário.

  • Open/Closed Principle (OCP): Novos testes E2E podem ser adicionados sem alterar os testes existentes.

  • Liskov Substitution Principle (LSP): Utiliza um banco de dados temporário, permitindo que o ambiente de teste seja totalmente controlado e isolado.

  • Dependency Inversion Principle (DIP): Os testes E2E são independentes dos detalhes de implementação, focando apenas no comportamento esperado da aplicação.

9. make-[entidade]-use-case.ts (ex: make-register-use-case.ts)

  • Single Responsibility Principle (SRP): Responsável por instanciar um caso de uso e suas dependências, fazendo uma única coisa.

  • Open/Closed Principle (OCP): A fábrica é extensível; se precisarmos adicionar novas dependências ou modificar as existentes, podemos fazê-lo sem alterar o caso de uso.

  • Liskov Substitution Principle (LSP): A fábrica pode instanciar qualquer repositório que siga a interface definida, permitindo fácil substituição.

  • Interface Segregation Principle (ISP): O caso de uso depende apenas das interfaces (neste caso, repositórios) de que realmente precisa.

  • Dependency Inversion Principle (DIP): O caso de uso não depende de uma implementação concreta do repositório, mas de uma abstração, facilitando a substituição e testes.

10. errors/[error].ts (ex: invalid-credentials-error.ts)

  • Single Responsibility Principle (SRP): Cada arquivo de erro é responsável por definir um tipo específico de erro, fazendo uma única coisa.

  • Open/Closed Principle (OCP): Novos tipos de erros podem ser adicionados sem alterar o código existente.

  • Liskov Substitution Principle (LSP): Eles podem ser substituídos ou estendidos para fornecer detalhes adicionais se necessário.

  • Interface Segregation Principle (ISP): Não aplicável neste contexto.

  • Dependency Inversion Principle (DIP): Os arquivos de erro podem ser importados como abstrações onde for necessário, tornando o código mais flexível.

Já enfrentei inúmeras dificuldades com a manutenção de APIs no passado e, sinceramente, não gostaria de repetir esses erros. Então decidi fazer um guia mais ou menos correlacionando cada arquivo principal com SOLID.

Como diz o ditado "mata a cobra e mostra o pau" 😂😂😂 segue um repositório com a implementação API-Rest-Node-SOLID.

Vamos la galera de sugestões para deixar isso melhor.

Grande abraço.

O app.ts parece ter várias responsabilidades distintas, o que pode ser um ponto de discussão em relação ao SRP. A configuração do servidor, registro de rotas e tratamento de erros são, de fato, responsabilidades diferentes. Isso me lembra um restaurante que serve pizza, sushi e hambúrgueres e diz que trabalha só com comida.

Estou destacando isto, por que achei curioso e interessante a aplicação dos princípios SOLID na estruturação do projeto, mas confesso que enxergo muito problemas. Parece que esta tentando encaixar princípios complexos em uma estrutura simplista - e sem necessidade.

É importante notar que os princípios foram desenvolvidos com base nas observações e práticas da programação orientada a objetos, e seu criador Uncle Bob, é uma figura proeminente nesse campo, principalmente na comunidade Java.

O Node.js, por outro lado, é uma plataforma assíncrona e orientada a eventos, o que a torna muito distinta da programação orientada a objetos tradicional.

O SOLID é uma ferramenta útil sim, em qualquer lingaugem, mas não é uma fórmula mágica para resolver todos os problemas. Lembre-se, no final do dia, o importante é criar código de qualidade, independentemente de quantos princípios SOLID estiver seguindo.