DEV Community

DevDen
DevDen

Posted on

revisitando arquitetura em camadas - separação de responsabilidades no backend

Esse ano eu decidi tirar um projeto do papel: parar de ficar apenas pintando botão na interface e rever conceitos básicos de programação e engenharia de software que, por falta de uso no dia a dia, acabaram se perdendo na memória.
O tópico da minha revisão hoje é arquitetura em camadas, mais especificamente a relação entre modules, services, controllers, repositories e clients.

E quem quiser refrescar a memória (ou dar pitaco!) também, vamo junto:

Module

É como o mise en place do serviço em questão. Pra quem não tem 300 vídeos de culinária salvos no youtube e não conhece o termo, mise en place é o preparo e arranjo de todos os ingredientes necessários antes que o prato comece a ser feito, e é exatamente isso que o module faz:

  • configura os providers
  • exporta serviços para uso em outros módulos
  • importa as dependências que os controllers, services e etc vão precisar tudo isso no start da aplicação.

O código lá dentro se parece mais ou menos com isso:

class UserModule {
    static create() {
        const repository = new UserRepository();
        const service = new UserService(repository);
        const controller = new UserController(service);

        return { repository, service, controller }
    }
}
Enter fullscreen mode Exit fullscreen mode

💡 Nota: em arquiteturas mais comuns em Node/Java (sem frameworks), o termo module nem sempre é usado para este conceito, muitas vezes se fala em inversão/injeção de dependência. Para fins deste artigo, module é uma abstração usada pra organizar dependências e inicialização de componente, mas pode ter outros significados em outros contextos.

Controller

O controller é o ponto de entrada que define os endpoints da API. É ele quem vai lidar com as requisições e as respostas, bem como definir que tipo de requisições os endpoints aceitam (GET, POST, DELETE, etc).

Aqui do ponto de vista de alguém que está totalmente não familiarizado com backend, o controller se destaca como a camada mais simpática para entender o que determinado serviço (ou contexto?) faz.

Outras coisas legais também caem sob a responsabilidade do controller:

  • transformar dados de resposta usando DTOs
  • delegar a lógica de negócio para os services
class UserController {
    constructor(userService) {
        this.userService = userService;
    }

    getUsers(){
        try {
            const users = this.userService.getAllUsers();
            return { success: true, data: users };
        } catch (error) {
            return { success: false, error: error.message };
        }
    }

    createUser(name, email) {
        try {
            const user = this.userService.createUser(name, email);
            return { success: true, data: user };
        } catch (error) {
            return { success: false, error: error.message };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Service

Falando nele, é aqui que a mágica acontece. Todas as operações pesadas e regras de negócio são definidas e realizadas dentro do service. O endpoint retorna dados filtrados de uma lista gigantesca? Provavelmente é o service que vai fazer essa filtragem. O controller, então, não precisa saber de nada complicado: basta chamar o método necessário que o service faz o trabalho todo (ou manda outra camada fazer, mas a gente chega lá daqui a pouco ;))

class UserService {
    constructor(userRepository) {
        this.userRepository = userRepository;
    }

    getAllUsers() {
        return this.userRepository.findAll();
    }

    createUser(name, email) {
        if(!email.includes("@")) {
            throw new Error("Invalid email");
        }

        return this.userRepository.create({ name, email });
    }
}
Enter fullscreen mode Exit fullscreen mode

Repository

É a camada responsável por acessar e fazer operações no banco de dados. Essencialmente abstrai as interações com o BD, encapsulando as queries e assim criando uma interface limpa para acessar dados.

class UserRepository {
    constructor() {
        this.users = [
            {id: 1, name: "John", email: "john@email.com"},
            {id: 2, name: "Jane", email: "jane@email.com"}
        ];
    }

    findAll() {
        return this.users;
    }

    create(user) {
        const newUser = { id:Date.now(), ...user };
        this.users.push(newUser);
        return newUser;
    }
}
Enter fullscreen mode Exit fullscreen mode

Client

Camada de comunicação com serviços externos. Se a aplicação precisa usar outras APIs, esse vai ser o ponto de contato entre ambos, então o Client fica normalmente responsável por:

  • fazer as requisições HTTP para os outros serviços
  • transformar os dados que recebe para que possam ser utilizados na aplicação
  • lidar com cache, headers e autenticação
class EmailClient {
    constructor(apiKey) {
        this.apiKey = apiKey;
    }

    async sendEmail(to, subject, message) {
        const response = await fetch('https://some-api.email-service.com/send', {
            method: 'POST', 
            headers: { 
                'Authorization': this.apiKey,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ to, subject, message })
        });

        if (!response.ok) {
            throw new Error('Email failed');
        }

        return response.json();
    }
}

Enter fullscreen mode Exit fullscreen mode

Em resumo:

Separando as responsabilidades em camadas, o fluxo acontece mais ou menos assim:

  1. Ao iniciar a aplicação (AppStart)
    • O Module cria e injeta dependências
      • O UserRepository é passado pro UserService
      • UserService é passado pro UserController
      • EmailClient pode ser injetado no UserService, se necessário
  2. Uma requisição é feita
    • Usuário -> Controller -> Service
    • Service busca os dados em Repository (banco de dados) ou Client (API externa)
    • A resposta volta pelo mesmo caminho até o usuário

E o module?

Ele não participa desse fluxo em tempo de execução, o module é chamado na inicialização da aplicação e garante que, quando a aplicação estiver de pé, todas as pecinhas dela estão prontas pra uso.

Top comments (0)