Pular para o conteúdo

Clean Architecture em Node.js com Express: Um guia prático para desenvolvedores

Clean Architecture é uma abordagem de design de software proposta por Robert C. Martin (também conhecido como “Uncle Bob”), que visa organizar o código em camadas separadas, cada uma com sua responsabilidade específica. Esta arquitetura permite criar sistemas escaláveis, testáveis e fáceis de manter. Neste artigo, discutiremos como implementar a Clean Architecture em um projeto Node.js utilizando o Express, um popular framework web minimalista.

O que é Clean Architecture?

A Clean Architecture tem como objetivo principal criar sistemas desacoplados e independentes de frameworks e bibliotecas externas. Ela divide o sistema em camadas concêntricas, onde cada camada depende apenas das camadas mais internas. Essas camadas são:

  1. Entidades: Representam os objetos de negócio e as regras de negócio que não dependem de detalhes externos.
  2. Casos de Uso: Orquestram as regras de negócio e coordenam o fluxo de dados entre as entidades e os detalhes externos.
  3. Adaptadores de Interface: Fazem a conversão entre os formatos de dados utilizados pelas entidades e os formatos exigidos pelos detalhes externos.
  4. Detalhes Externos: Incluem frameworks, bibliotecas, bancos de dados e outras fontes externas com as quais o sistema interage.

Configurando o ambiente

Para começar, crie um novo projeto Node.js e instale o Express:

$ mkdir clean-architecture-node-express
$ cd clean-architecture-node-express
$ npm init -y
$ npm install express

Estrutura de diretórios

Organize a estrutura de diretórios do projeto de acordo com as camadas da Clean Architecture:

clean-architecture-node-express
├── src
│   ├── entities
│   ├── use_cases
│   ├── interfaces
│   │   ├── controllers
│   │   ├── repositories
│   │   └── adapters
│   └── frameworks
│       └── express
└── tests

Exemplo: Sistema de gerenciamento de tarefas

Para ilustrar a implementação da Clean Architecture em Node.js com Express, criaremos um sistema simples de gerenciamento de tarefas. O sistema terá as seguintes funcionalidades:

  1. Criar tarefas
  2. Listar tarefas
  3. Atualizar o status de uma tarefa
  4. Deletar uma tarefa

Entidades

Crie a entidade Task no diretório entities:

// src/entities/Task.js

class Task {
  constructor(id, title, completed) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }
}

module.exports = Task;

Casos de Uso

Implemente os casos de uso para as funcionalidades do sistema no diretório use_cases:

// src/use_cases/CreateTask.js

class CreateTask {
  constructor(taskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(title) {
    return this.taskRepository.create(title);
  }
}

module.exports = CreateTask;

Faça o mesmo para os casos de uso ListTasks, UpdateTaskStatus e DeleteTask.

// src/use_cases/ListTasks.js

class ListTasks {
  constructor(taskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute() {
    return this.taskRepository.getAll();
  }
}

module.exports = ListTasks;
// src/use_cases/UpdateTaskStatus.js

class UpdateTaskStatus {
  constructor(taskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(id, completed) {
    return this.taskRepository.updateStatus(id, completed);
  }
}

module.exports = UpdateTaskStatus;
// src/use_cases/DeleteTask.js

class DeleteTask {
  constructor(taskRepository) {
    this.taskRepository = taskRepository;
  }

  async execute(id) {
    return this.taskRepository.delete(id);
  }
}

module.exports = DeleteTask;

Adaptadores de Interface

Crie adaptadores de interface para os repositórios no diretório interfaces/repositories. Por exemplo, para um repositório de tarefas em memória:

// src/interfaces/repositories/InMemoryTaskRepository.js

const Task = require('../../entities/Task');

class InMemoryTaskRepository {
  constructor() {
    this.tasks = [];
    this.idCounter = 1;
  }

  async create(title) {
    const task = new Task(this.idCounter++, title, false);
    this.tasks.push(task);
    return task;
  }

  async getAll() {
    return this.tasks;
  }

  async updateStatus(id, completed) {
    const task = this.tasks.find(task => task.id === id);
    if (task) {
      task.completed = completed;
      return task;
    }
    return null;
  }

  async delete(id) {
    const index = this.tasks.findIndex(task => task.id === id);
    if (index !== -1) {
      this.tasks.splice(index, 1);
      return true;
    }
    return false;
  }
}

module.exports = InMemoryTaskRepository;

Controllers

Crie os controladores no diretório interfaces/controllers. Estes controladores irão receber as requisições HTTP e interagir com os casos de uso:

// src/interfaces/controllers/TaskController.js

const express = require('express');
const CreateTask = require('../../use_cases/CreateTask');
const ListTasks = require('../../use_cases/ListTasks');
const UpdateTaskStatus = require('../../use_cases/UpdateTaskStatus');
const DeleteTask = require('../../use_cases/DeleteTask');
const InMemoryTaskRepository = require('../repositories/InMemoryTaskRepository');

const taskRepository = new InMemoryTaskRepository();
const router = express.Router();

router.post('/', async (req, res) => {
  const createTask = new CreateTask(taskRepository);
  const task = await createTask.execute(req.body.title);
  res.status(201).json(task);
});

router.get('/', async (req, res) => {
  const listTasks = new ListTasks(taskRepository);
  const tasks = await listTasks.execute();
  res.json(tasks);
});

router.patch('/:id/status', async (req, res) => {
  const updateTaskStatus = new UpdateTaskStatus(taskRepository);
  const task = await updateTaskStatus.execute(parseInt(req.params.id), req.body.completed);
  if (task) {
    res.json(task);
  } else {
    res.status(404).send('Task not found');
  }
});

router.delete('/:id', async (req, res) => {
  const deleteTask = new DeleteTask(taskRepository);
  const success = await deleteTask.execute(parseInt(req.params.id));
  if (success) {
    res.status(204).send();
  } else { res.status(404).send('Task not found'); } });

module.exports = router;

Express Framework

Configure o aplicativo Express no diretório `frameworks/express`:

// src/frameworks/express/app.js

const express = require('express');
const taskController = require('../../interfaces/controllers/TaskController');

const app = express();

app.use(express.json());
app.use('/tasks', taskController);

module.exports = app;

Iniciando o servidor

Adicione o seguinte script no arquivo package.json:

{
  "scripts": {
    "start": "node src/frameworks/express/app.js"
  }
}

Agora você pode iniciar o servidor usando o comando npm start e testar as rotas disponíveis.

Conclusão

Neste artigo, exploramos a aplicação da Clean Architecture em um projeto Node.js com Express. Através do exemplo de um sistema de gerenciamento de tarefas, demonstramos como organizar o código em camadas separadas, de acordo com suas responsabilidades, facilitando a manutenção e a escalabilidade do projeto.

A Clean Architecture é uma abordagem eficiente e flexível que pode ser adaptada a diferentes linguagens e frameworks, e seu uso pode melhorar significativamente a qualidade e a longevidade de seu código. Portanto, é importante considerar essa arquitetura ao desenvolver projetos de software, especialmente aqueles com expectativas de crescimento e evolução contínua.

3 comentários em “Clean Architecture em Node.js com Express: Um guia prático para desenvolvedores”

  1. Opa, tudo bem? Achei meio esquisito que o tutorial considera a arquitetura limpa, mas o controlador está fortemente ligado ao express. Seria interessante, para um projeto mais robusto, criar uma interface para o framework de servidor?

    1. Sim, até mesmo express pode ser adicionando a uma interface, nosso exemplo está fortemente ligado mesmo, porém pode-se fazer uma abstração para uma interface, afim de poder utilizar n frameworks de servidor como o fastify por exemplo.

      Obrigado pelo comentário!

  2. Bom dia!

    Excelente projeto, deu pra dar uma ideia de como funciona. Preciso de mais detalhes dessa arquitetura num projeto maior, com banco de dados, typescript, swagger pra poder visualizar melhor o conceito.

    Fora que pra mim ainda está confuso a camada de adaptadores de interface. Como eu usaria numa regra mais complexa por exemplo.

    Mas foi um excelente artigo, está de parabéns e muito obrigado pelo conteúdo!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.