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 nostart
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 }
}
}
💡 Nota: em arquiteturas mais comuns em Node/Java (sem frameworks), o termo
module
nem sempre é usado para este conceito, muitas vezes se fala eminversã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 };
}
}
}
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 });
}
}
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;
}
}
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();
}
}
Em resumo:
Separando as responsabilidades em camadas, o fluxo acontece mais ou menos assim:
- Ao iniciar a aplicação (AppStart)
- O
Module
cria e injeta dependências- O
UserRepository
é passado proUserService
-
UserService
é passado proUserController
-
EmailClient
pode ser injetado noUserService
, se necessário
- O
- O
- Uma requisição é feita
- Usuário ->
Controller
->Service
-
Service
busca os dados emRepository
(banco de dados) ouClient
(API externa) - A resposta volta pelo mesmo caminho até o usuário
- 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)