Clean arquitecture com nodejs e typescript

Está gostando? Compartilhe

A arquitetura de software é um conceito fundamental na engenharia de software. A escolha de uma arquitetura apropriada pode ter um impacto significativo na qualidade, manutenção e escalabilidade de um sistema. Uma arquitetura popular que ganhou destaque nos últimos anos é a Clean Architecture (Arquitetura Limpa), proposta por Robert C. Martin, também conhecido como “Uncle Bob”. Neste artigo, vamos explorar a Clean Architecture aplicada a projetos reais utilizando Node.js e TypeScript.

O que é Clean Architecture?

A Clean Architecture é um estilo arquitetural que promove a separação de preocupações e a independência de frameworks e bibliotecas externas. Ela é baseada em princípios como a Inversão de Dependências (Dependency Inversion), Separação de Interesses (Separation of Concerns) e Princípio da Responsabilidade Única (Single Responsibility Principle).

Em resumo, a Clean Architecture busca criar um sistema modular, com componentes independentes e bem definidos, facilitando a manutenção, testabilidade e escalabilidade do código. Além disso, ela enfatiza a clareza e a legibilidade do código, tornando-o mais fácil de entender e evoluir ao longo do tempo.

Princípios da Clean Architecture

A Clean Architecture é composta por várias camadas, cada uma com um conjunto específico de responsabilidades. Vamos detalhar as principais camadas e seus princípios:

  1. Entidades (Entities): Representam os objetos de negócio do sistema, como entidades de domínio, e são independentes de qualquer infraestrutura externa. Elas encapsulam as regras de negócio e contêm apenas a lógica de negócio pura.

  2. Casos de Uso (Use Cases): Representam as ações que podem ser realizadas no sistema, como a execução de uma determinada funcionalidade. Eles orquestram a interação entre as entidades e as interfaces do sistema. Os Casos de Uso são dependentes das Entidades e são independentes das interfaces externas, como o banco de dados, a camada de apresentação e outros detalhes de implementação.

  3. Interfaces (Interfaces): Representam os pontos de entrada e saída do sistema, como APIs, interfaces de usuário e serviços externos. Elas são responsáveis por adaptar os dados e as ações do sistema para a comunicação com o mundo externo. As Interfaces são dependentes dos Casos de Uso, mas não dependem das entidades.

  4. Frameworks e Drivers (Frameworks and Drivers): Representam as tecnologias externas utilizadas pelo sistema, como frameworks web, bibliotecas de acesso a banco de dados, serviços de terceiros, entre outros. Eles são responsáveis por fornecer a infraestrutura necessária para a execução do sistema. Os Frameworks e Drivers são dependentes das Interfaces, mas não dependem das entidades ou dos casos de uso.

Exemplo de aplicação real.

Vamos considerar um cenário de uma aplicação de e-commerce, onde temos as seguintes funcionalidades: cadastro de usuários, cadastro de produtos, adição de produtos ao carrinho, realização de pedidos e processamento de pagamentos. Vamos assumir que a aplicação será desenvolvida utilizando Node.js e TypeScript.

Organização do Projeto Ao aplicar a Clean Architecture a esse projeto, podemos organizar o código em camadas, seguindo os princípios da arquitetura limpa. Aqui está uma possível estrutura de diretórios para esse projeto:

				
					src/
  |- entities/
      |- User.ts
      |- Product.ts
      |- Cart.ts
      |- Order.ts
      |- Payment.ts
  |- usecases/
      |- user/
          |- CreateUserUseCase.ts
          |- GetUserUseCase.ts
      |- product/
          |- CreateProductUseCase.ts
          |- GetProductUseCase.ts
      |- cart/
          |- AddToCartUseCase.ts
          |- RemoveFromCartUseCase.ts
          |- GetCartUseCase.ts
      |- order/
          |- CreateOrderUseCase.ts
          |- GetOrderUseCase.ts
      |- payment/
          |- ProcessPaymentUseCase.ts
  |- interfaces/
      |- controllers/
          |- UserController.ts
          |- ProductController.ts
          |- CartController.ts
          |- OrderController.ts
          |- PaymentController.ts
      |- repositories/
          |- UserRepository.ts
          |- ProductRepository.ts
          |- CartRepository.ts
          |- OrderRepository.ts
          |- PaymentRepository.ts
  |- frameworks/
      |- express/
          |- routes/
              |- userRoutes.ts
              |- productRoutes.ts
              |- cartRoutes.ts
              |- orderRoutes.ts
              |- paymentRoutes.ts
          |- controllers/
              |- ExpressUserController.ts
              |- ExpressProductController.ts
              |- ExpressCartController.ts
              |- ExpressOrderController.ts
              |- ExpressPaymentController.ts
      |- typeorm/
          |- repositories/
              |- TypeORMUserRepository.ts
              |- TypeORMProductRepository.ts
              |- TypeORMCartRepository.ts
              |- TypeORMOrderRepository.ts
              |- TypeORMPaymentRepository.ts
      |- externalServices/
          |- paymentGateway.ts

				
			

Aqui, temos a pasta entities, que contém as entidades de domínio do sistema, como User, Product, Cart, Order e Payment. A pasta usecases contém os casos de uso do sistema, como CreateUserUseCase, GetProductUseCase, AddToCartUseCase, entre outros. A pasta interfaces contém as interfaces de comunicação do sistema, como controllers e repositories, que serão implementadas pelas camadas externas do sistema. A pasta frameworks contém a implementação dos detalhes de infraestrutura, como rotas e controllers específicos de uma biblioteca ou framework, como o Express ou o TypeORM.

Dependências e Fluxo de Dados Nesse exemplo, as entidades e os casos de uso não têm dependências externas, ou seja, são as camadas mais internas do sistema. Os casos de uso dependem das entidades, pois eles manipulam as regras de negócio do domínio. As interfaces, como os controllers e os repositories, dependem dos casos de uso, pois são responsáveis por adaptar as ações do sistema para a comunicação com o mundo externo. Por fim, os frameworks e drivers, como as rotas e controllers específicos do Express ou do TypeORM, dependem das interfaces para implementar a lógica de comunicação com o sistema.

O fluxo de dados ocorre de dentro para fora, ou seja, as informações fluem das camadas mais internas para as camadas mais externas. Por exemplo, quando um usuário faz uma requisição para criar um novo usuário na aplicação, a requisição é tratada pelo UserController, que utiliza o CreateUserUseCase para manipular a entidade User e criar um novo usuário no sistema. O CreateUserUseCase, por sua vez, pode utilizar o UserRepository para persistir os dados do novo usuário em um banco de dados, por exemplo.

A estrutura da Clean Architecture é representada por um conjunto de círculos concêntricos, onde as camadas internas representam as partes mais centrais e estáveis do sistema, e as camadas externas representam as partes mais externas e instáveis do sistema. A ideia é que as dependências sempre apontem para dentro, ou seja, as camadas mais internas não dependem das camadas mais externas, criando um fluxo unidirecional

Os Princípios da Clean Architecture nesse exemplo

A Clean Architecture é baseada em uma série de princípios que auxiliam na criação de um código limpo, testável e de fácil manutenção. Alguns desses princípios são:

  1. Separação de responsabilidades: cada componente do sistema deve ter uma única responsabilidade e não deve ter conhecimento detalhado sobre as implementações externas.

  2. Inversão de dependências: as dependências devem ser invertidas, ou seja, as camadas internas não devem depender das camadas externas. Isso permite a substituição de implementações externas sem afetar as camadas internas.

  3. Testabilidade: o código deve ser projetado de forma a ser facilmente testável, permitindo a escrita de testes unitários e de integração para garantir a qualidade do software.

  4. Separação de preocupações: a lógica de negócio do sistema deve estar separada das implementações de infraestrutura, como bancos de dados e frameworks, permitindo a alteração dessas implementações sem afetar a lógica de negócio.

Benefícios da Clean Architecture

A aplicação da Clean Architecture em um projeto real traz diversos benefícios, tais como:

  1. Manutenção facilitada: a separação clara de responsabilidades e a inversão de dependências tornam o código mais fácil de ser mantido, permitindo atualizações e correções de forma mais eficiente.

  2. Testabilidade: a arquitetura limpa facilita a escrita de testes unitários e de integração, permitindo a identificação precoce de problemas e a garantia da qualidade do software.

  3. Flexibilidade: a separação de preocupações e a inversão de dependências tornam o sistema mais flexível para adaptação a mudanças de requisitos ou substituição de tecnologias.

  4. Escalabilidade: a arquitetura limpa permite que o sistema seja escalável, pois a separação de responsabilidades e a inversão de dependências permitem que novos recursos sejam adicionados sem afetar a estrutura existente.

Segundo Exemplo

Vamos agora exemplificar a aplicação da Clean Architecture em um projeto real de gerenciamento de tarefas (to-do list) usando Node.js e TypeScript. Para isso, vamos seguir a estrutura de camadas descrita anteriormente, utilizando entidades, casos de uso, interfaces e frameworks/drivers.

  1. Entidades As entidades são as representações dos objetos de domínio do sistema. No nosso exemplo, teríamos a entidade Task, que pode ser definida como uma classe em TypeScript com as propriedades e métodos relevantes para uma tarefa, como o id, title, description, createdAt, updatedAt, e os métodos para realizar operações relacionadas a tarefas, como marcar como concluída, atualizar o título, etc.
				
					class Task {
  private id: number;
  private title: string;
  private description: string;
  private createdAt: Date;
  private updatedAt: Date;
  private isCompleted: boolean;

  // Construtor
  constructor(id: number, title: string, description: string) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.createdAt = new Date();
    this.updatedAt = new Date();
    this.isCompleted = false;
  }

  // Getters e Setters
  // ...

  // Métodos
  markAsCompleted(): void {
    this.isCompleted = true;
    this.updatedAt = new Date();
  }

  updateTitle(newTitle: string): void {
    this.title = newTitle;
    this.updatedAt = new Date();
  }

  // Outros métodos relevantes
  // ...
}

				
			
  1. Casos de Uso Os casos de uso são responsáveis por implementar a lógica de negócio do sistema. No nosso exemplo, teríamos os seguintes casos de uso:
  • CreateTaskUseCase: responsável por criar uma nova tarefa no sistema.
  • GetActiveTasksUseCase: responsável por obter todas as tarefas ativas do usuário.
  • MarkTaskAsCompletedUseCase: responsável por marcar uma tarefa como concluída.
  • UpdateTaskTitleUseCase: responsável por atualizar o título de uma tarefa.

Esses casos de uso dependem das entidades para realizar as operações necessárias. Eles podem ser implementados como classes em TypeScript, e cada um deles teria um método executável que implementa a lógica de negócio específica.

				
					class CreateTaskUseCase {
  private taskRepository: TaskRepository;

  constructor(taskRepository: TaskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(title: string, description: string): Promise<Task> {
    const task = new Task(1, title, description); // Simulação de criação de uma nova tarefa com id 1
    return this.taskRepository.createTask(task);
  }
}

class GetActiveTasksUseCase {
  private taskRepository: TaskRepository;

  constructor(taskRepository: TaskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(): Promise<Task[]> {
    return this.taskRepository.getActiveTasks();
  }
}

class MarkTaskAsCompletedUseCase {
  private taskRepository: TaskRepository;

  constructor(taskRepository: TaskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(taskId: number): Promise<Task> {
    const task = await this.taskRepository.getTaskById(taskId);
    task.markAsCompleted();
    return this.taskRepository.updateTask(task);
  }
}

class UpdateTaskTitleUseCase {
  private taskRepository: TaskRepository;

  constructor(taskRepository: TaskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(taskId: number, newTitle: string): Promise<Task> {
    const task = await this.taskRepository.getTaskById(taskId);
    task.updateTitle(newTitle);
    return this.taskRepository.updateTask(task);
  }
}

				
			
  1. Interfaces As interfaces são responsáveis por definir os contratos que os elementos externos ao domínio do sistema (como serviços externos, bancos de dados, etc.) devem cumprir. No nosso exemplo, teríamos as seguintes interfaces:
  • TaskRepository: define os métodos que o repositório de tarefas deve implementar para realizar operações de CRUD (Create, Read, Update, Delete) no banco de dados ou em outra forma de armazenamento persistente.
				
					interface TaskRepository {
  createTask(task: Task): Promise<Task>;
  getTaskById(taskId: number): Promise<Task | null>;
  getActiveTasks(): Promise<Task[]>;
  updateTask(task: Task): Promise<Task>;
  deleteTask(taskId: number): Promise<void>;
}

				
			
  1. Frameworks/Drivers Os frameworks/drivers são responsáveis por implementar os detalhes técnicos do sistema, como as interfaces com o banco de dados, a interface de usuário, etc. No nosso exemplo, poderíamos ter os seguintes frameworks/drivers:
  • TaskRepositoryDB: uma implementação concreta da interface TaskRepository que realiza operações de CRUD no banco de dados. Poderia ser implementada usando um ORM (Object-Relational Mapping) como TypeORM ou Sequelize, por exemplo.
				
					class TaskRepositoryDB implements TaskRepository {
  // Implementação dos métodos da interface TaskRepository usando um ORM, por exemplo
  // ...
}

				
			
  1. TaskRepositoryMemory: uma implementação concreta da interface TaskRepository que realiza operações de CRUD em memória, sem persistência no banco de dados. Poderia ser usada para fins de testes ou desenvolvimento.
				
					class TaskRepositoryMemory implements TaskRepository {
  private tasks: Task[];

  constructor() {
    this.tasks = [];
  }

  async createTask(task: Task): Promise<Task> {
    this.tasks.push(task);
    return task;
  }

  async getTaskById(taskId: number): Promise<Task | null> {
    const task = this.tasks.find(t => t.getId() === taskId);
    return task ? task : null;
  }

  async getActiveTasks(): Promise<Task[]> {
    return this.tasks.filter(t => !t.isCompleted());
  }

  async updateTask(task: Task): Promise<Task> {
    const index = this.tasks.findIndex(t => t.getId() === task.getId());
    if (index !== -1) {
      this.tasks[index] = task;
      return task;
    } else {
      throw new Error('Task not found');
    }
  }

  async deleteTask(taskId: number): Promise<void> {
    this.tasks = this.tasks.filter(t => t.getId() !== taskId);
  }
}

				
			

5.  No exemplo anterior, temos uma dependência unidirecional entre as camadas, onde a camada de uso (Use Cases) depende da interface (contrato) da camada de interface (Interfaces), e a camada de interface depende da implementação concreta da camada de Frameworks/Drivers. Essa abordagem permite que a camada de uso seja isolada de detalhes de implementação técnica, tornando-a independente de frameworks, bancos de dados ou outras tecnologias específicas.

Além disso, é importante respeitar a direção da dependência, que sempre vai de dentro para fora. Ou seja, as camadas mais internas (domínio) não devem depender das camadas mais externas (interfaces e frameworks/drivers). Isso permite uma maior flexibilidade e manutenibilidade do sistema, pois é mais fácil substituir ou modificar as camadas externas sem afetar o domínio do sistema.

Outro princípio importante da arquitetura limpa é a inversão de dependência (Dependency Inversion), que consiste em depender de abstrações (interfaces e contratos) em vez de depender de implementações concretas. Isso facilita a substituição de implementações e a realização de testes unitários, uma vez que é possível substituir as implementações concretas por mocks ou stubs durante os testes.

  1. Exemplo de aplicação real com Clean Architecture em Node.js e TypeScript Vamos agora exemplificar como a arquitetura limpa pode ser aplicada em um projeto real em Node.js e TypeScript. Vamos supor que estamos desenvolvendo uma aplicação de lista de tarefas (to-do list) e queremos aplicar os conceitos de Clean Architecture.
				
					my-todo-app/
|-- src/
|   |-- domain/
|   |   |-- models/
|   |   |   |-- Task.ts
|   |   |-- repositories/
|   |   |   |-- TaskRepository.ts
|   |   |-- useCases/
|   |   |   |-- CreateTaskUseCase.ts
|   |   |   |-- GetTaskByIdUseCase.ts
|   |   |   |-- GetActiveTasksUseCase.ts
|   |   |   |-- MarkTaskAsCompletedUseCase.ts
|   |   |   |-- UpdateTaskTitleUseCase.ts
|   |-- interfaces/
|   |   |-- controllers/
|   |   |   |-- TaskController.ts
|   |   |-- repositories/
|   |   |   |-- TaskRepositoryDB.ts
|   |   |-- routes/
|   |   |   |-- taskRoutes.ts
|   |-- frameworks/
|   |   |-- database/
|   |   |   |-- index.ts
|   |   |-- express/
|   |   |   |-- server.ts
|   |   |   |-- middlewares/
|   |   |   |   |-- errorMiddleware.ts
|   |   |   |   |-- validationMiddleware.ts
|-- tests/
|   |-- domain/
|   |   |-- models/
|   |   |   |-- Task.test.ts
|   |-- interfaces/
|   |   |-- repositories/
|   |   |   |-- TaskRepositoryDB.test.ts
|   |   |-- routes/
|   |   |   |-- taskRoutes.test.ts

				
			
  • Na pasta src/domain ficam os modelos de domínio, como a entidade Task, que representa uma tarefa na aplicação.

  • Na pasta src/domain/repositories ficam as interfaces dos repositórios, como o TaskRepository, que define os métodos para realizar operações relacionadas a tarefas, como criar, atualizar, buscar e marcar como concluída.

  • Na pasta src/domain/useCases ficam os casos de uso da aplicação, como CreateTaskUseCase, GetTaskByIdUseCase, GetActiveTasksUseCase e outros, que encapsulam as regras de negócio relacionadas às operações de tarefas.

  • Na pasta src/interfaces/controllers ficam os controladores da aplicação, como o TaskController, que é responsável por receber as requisições HTTP e chamar os casos de uso correspondentes.

  • Na pasta src/interfaces/repositories ficam as implementações concretas dos repositórios, como o TaskRepositoryDB, que é responsável por implementar a interface TaskRepository e realizar as operações de acesso a banco de dados.

  • Na pasta src/interfaces/routes ficam as rotas da aplicação, como o taskRoutes, que define as rotas HTTP relacionadas a tarefas e faz a chamada aos controladores correspondentes.

  • Na pasta src/frameworks/database ficam as configurações e conexões com o banco de dados, como o arquivo index.ts, que é responsável por criar a conexão com o banco de dados.

  • Na pasta src/frameworks/express ficam as configurações e middlewares do framework Express, como o arquivo server.ts, que é responsável por configurar o servidor HTTP e os middlewares, como o middleware de tratamento de erros e validação.

  • Na pasta tests/domain ficam os testes unitários para os modelos de domínio, como o Task.test.ts, que testa as funcionalidades da entidade Task.

  • Na pasta tests/interfaces/repositories ficam os testes unitários para as implementações dos repositórios, como o TaskRepositoryDB.test.ts, que testa as operações de acesso a banco de dados do TaskRepositoryDB.

  • Na pasta tests/interfaces/routes ficam os testes de integração para as rotas da aplicação, como o taskRoutes.test.ts, que testa as rotas HTTP relacionadas a tarefas e suas interações com os controladores e casos de uso.

Essa é apenas uma estrutura de exemplo e a organização pode variar de acordo com as necessidades e preferências de cada projeto. O importante é seguir os princípios e conceitos da arquitetura limpa, mantendo a separação de responsabilidades entre as camadas e permitindo a substituição de implementações sem afetar o domínio da aplicação.

Espero que isso tenha fornecido uma compreensão clara de como aplicar a Clean Architecture em um projeto real usando Node.js e TypeScript. A arquitetura limpa é uma abordagem poderosa para desenvolvimento de software, que promove a manutenibilidade, flexibilidade e testabilidade do código, permitindo criar sistemas de software robustos e escaláveis.

Conclusão

A Clean Architecture é uma abordagem de desenvolvimento de software que promove a criação de código limpo, testável e de fácil manutenção, através da separação clara de responsabilidades, inversão de dependências e foco na lógica de negócio do sistema. Com a utilização de conceitos como entidades, casos de uso, interfaces e frameworks/drivers, é possível criar sistemas flexíveis, escaláveis e de alta qualidade.

Para exemplificar a aplicação da Clean Architecture em projetos reais com Node.js e TypeScript, vamos considerar um cenário hipotético de uma aplicação de gerenciamento de tarefas (to-do list). Vamos supor que a aplicação precisa permitir que os usuários criem tarefas, visualizem suas tarefas ativas e concluam tarefas já realizadas.

A seguir, podemos descrever como as diferentes camadas da Clean Architecture seriam aplicadas nesse cenário:

  1. Entidades: As entidades representariam os objetos de domínio do sistema, como a própria tarefa. A entidade Task poderia ter propriedades como id, title, description, createdAt, updatedAt, etc., e métodos para realizar operações relacionadas a tarefas, como marcar como concluída, atualizar o título, etc.

  2. Casos de Uso: Os casos de uso seriam responsáveis pela manipulação da lógica de negócio do sistema. Por exemplo, teríamos um caso de uso CreateTaskUseCase que seria responsável por criar uma nova tarefa no sistema, um caso de uso GetActiveTasksUseCase que seria responsável por obter todas as tarefas ativas do usuário, e assim por diante. Esses casos de uso dependeriam das entidades para realizar as operações necessárias.

  3. Interfaces: As interfaces seriam responsáveis por adaptar a comunicação do sistema com o mundo externo. Por exemplo, teríamos um TaskController que receberia as requisições HTTP relacionadas a tarefas, utilizaria os casos de uso apropriados para tratar as requisições e retornar as respostas adequadas. O TaskController dependeria dos casos de uso para realizar as operações necessárias.

  4. Frameworks/Drivers: Os frameworks e drivers seriam responsáveis por implementar a comunicação do sistema com a infraestrutura externa, como banco de dados, APIs externas, etc. Por exemplo, teríamos um TaskRepository que seria responsável por persistir as tarefas no banco de dados. Esse repositório implementaria uma interface definida nos casos de uso, permitindo a substituição do banco de dados sem afetar a lógica de negócio.

Essa é apenas uma abordagem básica de como a Clean Architecture pode ser aplicada em uma aplicação real com Node.js e TypeScript. É importante ressaltar que a implementação detalhada pode variar de acordo com as necessidades e requisitos específicos do projeto. O importante é seguir os princípios da Clean Architecture, como a separação de responsabilidades, inversão de dependências e testabilidade, para criar um código limpo, organizado e de fácil manutenção.

Espero que este artigo tenha proporcionado uma visão geral sobre a aplicação da Clean Architecture em projetos reais usando Node.js e TypeScript, e que possa ser útil para sua compreensão e aplicação em seus próprios projetos. Lembre-se de estudar a fundo os conceitos e princípios da Clean Architecture e adaptá-los às necessidades específicas do seu projeto, sempre buscando aprimorar a qualidade e manutenibilidade do código.

Quer ficar atualizado sobre marketing ?

Assine a nossa newsletter.

Aproveite e Veja Também

DESCUBRA NESSE E-BOOK 5 ESTRATÉGIAS PARA VENDER MUITO NA SUA LOJA DROPSHIPPING.

Você Quer Impulsionar Seu Negócio?

Mande-nos uma mensagem que entramos em contato.

Desenvolvimento de Backend com Node.js, TypeScript, MongoDB e Docker: Práticas Avançadas com TDD, DDD, Clean Architecture e SOLID

DESCUBRA como desenvolver um Backend com Node.js, TypeScript, MongoDB e Docker utilizando práticas avançadas com TDD, DDD, Clean Architecture e SOLID.