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.