Software · Arquitetura de Software

Padrões Arquiteturais Web: Da Separação MVC ao Padrão REPR em APIs Modernas

A evolução da estruturação de backends web: da separação clássica MVC e arquiteturas em camadas até o padrão REPR (Request-Endpoint-Response), Feature Folders e persistência moderna com ORM.

Comparativo arquitetural entre a divisão horizontal em camadas MVC e a divisão vertical em fatias com o padrão REPR

A história das aplicações web e dos backends corporativos é marcada pela constante busca por modularidade, testabilidade e separação de responsabilidades. No início dos anos 2000, o modelo de arquitetura em três camadas e o padrão Model-View-Controller (MVC) revolucionaram o desenvolvimento de software ao separar a apresentação visual da lógica de negócios e do acesso ao banco de dados.

Contudo, a ascensão do desenvolvimento moderno de APIs REST, interfaces ricas orientadas a componentes no frontend (React, Angular, Vue) e microsserviços distribuídos revelou as limitações dos controladores inflados (Fat Controllers). É nesse cenário que emergem abordagens contemporâneas como o padrão REPR (Request-Endpoint-Response), arquiteturas baseadas em fatias verticais (Vertical Slice Architecture) e estruturas em Feature Folders.


O Modelo Cliente-Servidor e a Separação em Camadas

No paradigma clássico Cliente-Servidor, o sistema é dividido em nós consumidores de serviços (clientes navegadores, mobile ou outras aplicações) e nós provedores (servidores de backend e instâncias de banco de dados):

flowchart TD
    subgraph Clientes["Camada de Apresentação / Clientes"]
        Browser["Single Page Application (Astro/Angular/React)"]
        Mobile["Aplicativo Mobile (iOS/Android)"]
    end

    subgraph API["Servidor de Aplicação Web / Backend"]
        Endpoint["API Gateway / Endpoints HTTP REST & gRPC"]
        Negocio["Camada de Serviços / Lógica de Negócio"]
        AcessoDados["Camada de Repositórios & ORM"]
    end

    subgraph Persistencia["Armazenamento & Integração"]
        DB[("Banco de Dados Relacional / NoSQL")]
        Broker["Message Broker (RabbitMQ / Kafka)"]
    end

    Clientes -->|Requisição HTTP/JSON| Endpoint
    Endpoint --> Negocio
    Negocio --> AcessoDados
    AcessoDados --> DB
    Negocio -.->|"Eventos Assíncronos (Pub/Sub)"| Broker

A arquitetura em camadas tradicional (Layered Architecture) organiza o código por sua responsabilidade técnica:

  1. Presentation Layer (Apresentação): Controladores, roteamento HTTP e serialização de respostas.
  2. Business/Service Layer (Negócio): Regras de validação, coordenação de transações e fluxos operacionais.
  3. Data Access Layer (Persistência): DAOs, Repositórios e abstrações de Object-Relational Mapping (ORM).

A regra estrita da arquitetura em camadas dita que uma camada superior só pode conhecer a camada imediatamente inferior, promovendo um desacoplamento unidirecional.


A Anatomia do MVC: Triunfo e Degradação

O padrão MVC (Model-View-Controller) distribui o fluxo da aplicação em três vértices:

  • Model: Encapsula o estado e as regras de negócio dos dados.
  • View: A renderização visual retornada ao usuário (HTML nos frameworks clássicos como Rails, Django ou ASP.NET MVC).
  • Controller: O orquestrador que intercepta o input do usuário, invoca o modelo e seleciona a view apropriada.
flowchart LR
    User([Usuário]) -->|HTTP Request| C[Controller]
    C -->|Atualiza / Consulta| M[(Model)]
    M -->|Retorna Dados| C
    C -->|Passa ViewModel| V[View Engine]
    V -->|HTML Renderizado| User

O Fenômeno do "Fat Controller"

Quando o MVC foi transposto diretamente para APIs REST (onde a "View" se reduziu a um mero serializador JSON), os Controllers começaram a concentrar responsabilidades indevidas:

  • Um único arquivo UserController.cs ou UserController.ts acumula 15 endpoints distintos (CreateUser, ResetPassword, UploadAvatar, ListUsersWithPagination, etc.).
  • O construtor do controlador passa a exigir dezenas de dependências via Injeção de Dependência (IoC), degradando a manutenibilidade e os testes unitários.

O Padrão REPR: Request-Endpoint-Response

Para resolver a sobrecarga de controladores monolíticos, o padrão REPR (cunhado por Steve Smith e adotado amplamente em bibliotecas como FastEndpoints, Carter e MediatR no ecossistema .NET, além de frameworks Node/Go) propõe que APIs web sejam concebidas como uma coleção de funções isoladas: 1 Endpoint = 1 Requisição + 1 Resposta.

flowchart LR
    subgraph REPR["Padrão REPR (Uma Fatia Vertical Isolada)"]
        Req["Request DTO<br/>(Validação estrita e tipagem da entrada)"]
        Endp["Endpoint Handler<br/>(Lógica coesa e única daquela operação)"]
        Resp["Response DTO<br/>(Contrato explícito de payload de saída)"]

        Req --> Endp --> Resp
    end
Critério Padrão MVC Tradicional Padrão REPR / Vertical Slice
Organização do Código Agrupamento por pastas técnicas (Controllers/, Services/, Models/). Agrupamento por capacidade de negócio em Feature Folders (Features/Orders/CreateOrder/).
Coesão Baixa; o controller mistura operações de leitura, criação e deleção de um recurso. Máxima; o endpoint contém apenas a lógica específica daquele caso de uso.
Injeção de Dependência Construtor sobrecarregado com 8+ serviços que nem todo método utiliza. Cada handler injeta estritamente os 1 ou 2 serviços que sua operação requer.
Conflitos de Git (PRs) Constantes; múltiplos desenvolvedores alterando o mesmo arquivo de controlador. Quase nulos; cada nova rota é um novo arquivo isolado no repositório.

Exemplo do Padrão REPR em C# Moderno

// Features/Users/RegisterUser/RegisterUserModels.cs
public record RegisterUserRequest(string Email, string Password, string FullName);
public record RegisterUserResponse(Guid Id, string Email, DateTime CreatedAt);

// Features/Users/RegisterUser/RegisterUserEndpoint.cs
public class RegisterUserEndpoint : Endpoint<RegisterUserRequest, RegisterUserResponse>
{
    private readonly IUserRepository _repository;
    private readonly IPasswordHasher _hasher;

    public RegisterUserEndpoint(IUserRepository repository, IPasswordHasher hasher)
    {
        _repository = repository;
        _hasher = hasher;
    }

    public override void Configure()
    {
        Post("/api/v1/users");
        AllowAnonymous();
    }

    public override async Task HandleAsync(RegisterUserRequest req, CancellationToken ct)
    {
        if (await _repository.ExistsByEmailAsync(req.Email, ct))
        {
            AddError(r => r.Email, "E-mail já cadastrado na plataforma.");
            await SendErrorsAsync(cancellation: ct);
            return;
        }

        var user = new User(req.Email, _hasher.Hash(req.Password), req.FullName);
        await _repository.SaveAsync(user, ct);

        await SendCreatedAtAsync<GetUserByIdEndpoint>(
            new { id = user.Id }, 
            new RegisterUserResponse(user.Id, user.Email, user.CreatedAt), 
            cancellation: ct
        );
    }
}

Estratégias de Persistência: Database-First vs. Code-First em ORMs

A camada de acesso a dados e os frameworks ORM (Object-Relational Mapping, como Entity Framework Core, Hibernate ou Prisma) oferecem duas estratégias diametralmente opostas de ciclo de desenvolvimento:

flowchart TD
    subgraph DBFirst["Abordagem Database-First"]
        D1["1. DBA / Engenheiro cria tabelas e triggers no SGBD"]
        D2["2. Ferramenta de Scaffold gera entidades de código"]
        D1 --> D2
    end

    subgraph CodeFirst["Abordagem Code-First"]
        C1["1. Engenheiro projeta classes de domínio no código"]
        C2["2. Motor de Migrations gera DDL e altera o banco"]
        C1 --> C2
    end
  1. Database-First: Ideal para sistemas legados consolidados onde o esquema relacional já existe, é compartilhado por múltiplos sistemas externos e gerenciado exclusivamente por DBAs experientes.
  2. Code-First: O código da aplicação é a fonte única da verdade (Single Source of Truth). O desenvolvedor define as entidades em sua linguagem nativa e o pipeline de Migrations reflete as alterações de esquema de forma versionável no Git.

Comunicação Assíncrona: Message Brokers e Padrão Pub/Sub

Conforme o ecossistema web cresce, chamadas HTTP síncronas entre serviços encadeados criam acoplamento temporal e pontos únicos de falha (Cascading Failures). A introdução de comunicação orientada a mensagens assíncronas resolve esse desafio:

flowchart LR
    API["API de Checkout<br/>(Publisher)"]
    Queue[("Message Broker<br/>RabbitMQ / Kafka")]
    Worker1["Serviço de Faturamento<br/>(Consumer 1)"]
    Worker2["Serviço de Notificação<br/>(Consumer 2)"]
    Worker3["Serviço de Estoque<br/>(Consumer 3)"]

    API -->|Evento: PedidoCriado| Queue
    Queue --> Worker1
    Queue --> Worker2
    Queue --> Worker3
  • Message Queues (Filas ponto a ponto): Garantem que cada mensagem seja consumida por exatamente um trabalhador, ideal para distribuição uniforme de carga de tarefas pesadas.
  • Publish-Subscribe (Pub/Sub): O produtor emite um evento de domínio sem conhecer os destinatários; múltiplos assinantes processam o mesmo evento de forma independente e isolada.

Conclusão

A arquitetura de software para a web não possui fórmulas universais, mas sim uma trajetória clara rumo à autonomia e à redução de complexidade acidental. Ao abandonar controladores monolíticos em favor do padrão REPR e de fatias verticais, desacoplar dependências por meio de Inversão de Controle e Repositórios bem definidos, e isolar fluxos pesados através de mensageria assíncrona, engenheiros constroem plataformas resilientes que suportam evolução acelerada sem medo de quebras colaterais.