Veja uma imagem, extraída deste artigo, da organização dessa arquitetura:
Alguns insights sobre cada camada:
userInterface: Essa é a camada que vai se comunicar com o usuário, e só nela devemos deixar rastro do que é usado para isso. Exemplo: objetos do express, como o req e res, não podem sair daí. Controllers, ficam aí também. A função startServer também deve ficar aí, porque tudo isso tem a ver com a interface com o usuário. A declaração de injeção de dependencia, para simplicidade, também pode ficar aí por enquanto.
ApplicationService: Nesta camada vamos colocar os "handlers" da aplicação. Cada handler representa um "comando". Se você tiver uma rota GET something, por exemplo, você cria um GetSomethingHandler. Esta é uma camada abstrata de negócio, e não devemos inserir nada nela que se refira a interface com o usuário ou infraestrutura. As únicas coisas que você injeta nos handlers, são serviços de domínio (no máximo o logger, se achar que faz sentido). A responsabilidade do handler é orquestrar a dinâmica entre serviços de domínio. Exemplo: se você tiver dois serviços, cada um com uma responsabilidade diferente, e ambos precisam ser usados para atender certo comando, é o handler que chama eles. Um serviço não chama o outro, é responsabilidade do handler chamar um, pegar o retorno que for necessário, e chamar o outro;
DomainService: Nesta camada os serviços de domínio são inseridos. Aqui também, você pode declarar classes abstratas que representam repositórios por exemplo, não podemos ter nenhuma referência direta a elementos de infraestrutura (como mongo, express), nesta camada, qualquer coisa que é de infra, você cria uma classe abstrata aqui e isso irá ser seu símbolo de injeção. A implementação da interface você fará em outra camada. Você também pode criar classes de parser aqui, que podem ser injetadas nos DomainService, caso precise;
DomainModel: Nesta camada você não coloca código, apenas contratos, interfaces de objetos (não de serviços, nada com métodos). Os modelos de dados que você usa na sua aplicação, mas, de novo:: sem fazer referência a nenhum elemento de infra (como mongoose, por exemplo);
Infrastructure: Nesta camada, você implementa as interfaces de infraestrutura que foram declaradas no DomainService.
Test: a camada de teste falaremos em outro momento.
É importante seguir essas regras ao organiar o projeto assim:
DomainModel não pode fazer referência a nenhuma outra camada, nem a bibliotecas externas;
DomainService só pode fazer referência a DomianModel, nada mais;
ApplicationService só pode fazer referência a DomainService e DomainModel;
Infrastructure só pode fazer referência a DomainService e DomainModel;
UserInterface só pode fazer referência a ApplicationService e DomainModel;
Quando uma classe de repositorio é criada, você pode fazer algo assim, por exemplo:
É estranho usar classe como interface, mas isso te trás uma vantagem: na hora de declarar a injeção, você pode usar a classe abstrata como símbolo de injeção e deixar seu código mais limpo:
Isso dispensa o uso do decorator @inject. O @injectable ainda será necessário nas classes concretas, independente da camada. Isso é uma exceção às regras citadas acima. Algumas bibliotecas externas também podem ser consideradas exceção e serem usadas em qualquer camada, caso o papel delas não tenha a ver com infra estrutura, como as libs moment ou uuid, por exemplo.
Finalmente, agora que você chegou nesse ponto onde o projeto tá todo usando injeção, pegue essas informações e organize melhor o projeto, para seguir essas regras arquiteturais!
Para uma boa mantenabilidade, o ideal é que o projeto seja separado em camadas com responsabilidades claras, e sem invasão de escopo entre elas.
Uma arquitetura que funciona bem com typescript é a onion. Abaixo, há um artigo que explica como usá-la com inversify. https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad
Veja uma imagem, extraída deste artigo, da organização dessa arquitetura:
Alguns insights sobre cada camada:
É importante seguir essas regras ao organiar o projeto assim:
Quando uma classe de repositorio é criada, você pode fazer algo assim, por exemplo:
Então, na infrastructure, pode implementar ela, tratando ela como se fosse interface (o typescript permite isso):
É estranho usar classe como interface, mas isso te trás uma vantagem: na hora de declarar a injeção, você pode usar a classe abstrata como símbolo de injeção e deixar seu código mais limpo:
E aí, na hora de injetar:
Isso dispensa o uso do decorator @inject. O @injectable ainda será necessário nas classes concretas, independente da camada. Isso é uma exceção às regras citadas acima. Algumas bibliotecas externas também podem ser consideradas exceção e serem usadas em qualquer camada, caso o papel delas não tenha a ver com infra estrutura, como as libs moment ou uuid, por exemplo.
Finalmente, agora que você chegou nesse ponto onde o projeto tá todo usando injeção, pegue essas informações e organize melhor o projeto, para seguir essas regras arquiteturais!