Arquitetura no Frontend
Camadas no cliente
Seção intitulada “Camadas no cliente”A separação hexagonal transporta-se quase literalmente para o front, em três camadas:
- Domínio / lógica — regras de negócio do cliente: custom hooks, use cases, stores, funções puras. Não conhece React de renderização nem HTTP.
- UI — componentes que renderizam e capturam eventos. Idealmente “burros”: recebem dados e callbacks.
- Infraestrutura — adapters concretos: API clients,
localStorage, WebSocket, SDKs de terceiros.
A regra de dependência é a mesma da Clean Architecture: a UI e o domínio dependem de abstrações (portas); a infraestrutura implementa essas portas. O domínio nunca importa axios.
flowchart LR
subgraph UI["Camada UI"]
C[Componentes React]
end
subgraph DOM["Domínio / Lógica"]
H[Hooks / Use Cases]
P["Porta: UserRepository (interface)"]
end
subgraph INFRA["Infraestrutura"]
A[HttpUserRepository - adapter]
F[FakeUserRepository - teste]
end
C --> H
H --> P
A -. implementa .-> P
F -. implementa .-> P
A --> API[(REST/GraphQL)]
Ponto-chave: o API client é um adapter atrás de uma porta
Seção intitulada “Ponto-chave: o API client é um adapter atrás de uma porta”Em vez de o componente (ou o hook) chamar fetch direto, ele depende de uma interface (porta). A implementação real com HTTP é apenas um adapter. Isso é DIP - Dependency Inversion Principle literal: o de-alto-nível (domínio) e o de-baixo-nível (HTTP) dependem ambos da abstração.
O ganho é Testabilidade e Arquitetura: em teste, injeta-se um fake que retorna dados em memória — sem rede, sem MSW, instantâneo e determinístico.
// domain/UserRepository.ts (a PORTA)export type User = { id: string; name: string };
export interface UserRepository { getById(id: string): Promise<User>;}// infra/HttpUserRepository.ts (o ADAPTER real)import type { User, UserRepository } from "../domain/UserRepository";
export class HttpUserRepository implements UserRepository { constructor(private readonly baseUrl: string) {}
async getById(id: string): Promise<User> { const res = await fetch(`${this.baseUrl}/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }}// domain/RepositoryContext.tsx (a porta de injeção pertence ao domínio = DIP)import { createContext, useContext, type ReactNode } from "react";import type { UserRepository } from "./UserRepository";
const RepoContext = createContext<UserRepository | null>(null);
export function RepositoryProvider({ repository, children,}: { repository: UserRepository; children: ReactNode;}) { return <RepoContext.Provider value={repository}>{children}</RepoContext.Provider>;}
export function useUserRepository(): UserRepository { const repo = useContext(RepoContext); if (!repo) throw new Error("RepositoryProvider ausente"); return repo;}// domain/useUser.ts (use case como hook — depende da PORTA, não do adapter)import { useEffect, useState } from "react";import type { User } from "./UserRepository";import { useUserRepository } from "./RepositoryContext";
export function useUser(id: string) { const repo = useUserRepository(); const [user, setUser] = useState<User | null>(null); const [error, setError] = useState<Error | null>(null);
useEffect(() => { let active = true; repo .getById(id) .then((u) => active && setUser(u)) .catch((e) => active && setError(e as Error)); return () => { active = false; }; }, [id, repo]);
return { user, error };}// ui/UserName.tsx (Presentational — só renderiza)import type { User } from "../domain/UserRepository";
export function UserName({ user }: { user: User | null; }) { if (!user) return <span>Carregando…</span>; return <strong>{user.name}</strong>;}
// ui/UserNameContainer.tsx (Container — liga lógica à apresentação)import { useUser } from "../domain/useUser";import { UserName } from "./UserName";
export function UserNameContainer({ id }: { id: string }) { const { user, error } = useUser(id); if (error) return <span role="alert">Erro ao carregar</span>; return <UserName user={user} />;}O teste então injeta um fake — sem tocar a rede:
import { render, screen } from "@testing-library/react";import { RepositoryProvider } from "../domain/RepositoryContext";import { UserNameContainer } from "./UserNameContainer";import type { UserRepository } from "../domain/UserRepository";
const fakeRepo: UserRepository = { getById: async (id) => ({ id, name: "Grace Hopper" }),};
test("renderiza o nome vindo do repositório injetado", async () => { render( <RepositoryProvider repository={fakeRepo}> <UserNameContainer id="7" /> </RepositoryProvider> ); expect(await screen.findByText("Grace Hopper")).toBeInTheDocument();});Design Patterns clássicos no front
Seção intitulada “Design Patterns clássicos no front”- Container / Presentational — separa “de onde vêm os dados” (container, com lógica) de “como aparecem” (presentational, puro). É SRP - Single Responsibility Principle em nível de componente e facilita reuso e teste. Exemplo acima.
- Observer — o coração de todo state management reativo (Redux, Zustand, RxJS, signals). A store é o subject; componentes que assinam via seletor são os observers, re-renderizando quando o estado muda.
- Strategy — trocar comportamento sem
if/elseproliferando. Ex.: uma tabela cujo algoritmo de ordenação/formatação é passado como função; um formulário cuja regra de validação é injetada. É OCP - Open-Closed Principle: aberto a novas estratégias, fechado a modificação.
// Strategy: formatação injetada, componente fechado a mudançatype PriceFormatter = (cents: number) => string;
const brl: PriceFormatter = (c) => (c / 100).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
const usd: PriceFormatter = (c) => (c / 100).toLocaleString("en-US", { style: "currency", currency: "USD" });
function Price({ cents, format }: { cents: number; format: PriceFormatter }) { return <span>{format(cents)}</span>;}// <Price cents={19990} format={brl} /> → R$ 199,90SOLID aplicado a componentes
Seção intitulada “SOLID aplicado a componentes”- SRP - Single Responsibility Principle: um componente = uma razão para mudar. Buscar dados, calcular regra e renderizar são três razões; separe-as (container vs presentational vs use case).
- OCP - Open-Closed Principle: estender via
props, composição (children) e Strategy, não editando o componente base. - DIP - Dependency Inversion Principle: componentes dependem de portas (interfaces) injetadas por
props/Context, não de módulos concretos importados. É o que habilita o fake acima.
Contraste com Clean Architecture — e quando é overkill
Seção intitulada “Contraste com Clean Architecture — e quando é overkill”Clean Architecture no front (entities → use cases → interface adapters → frameworks) é possível e valiosa quando o cliente concentra muita regra de negócio: um editor complexo, uma calculadora fiscal, um SPA offline-first com sincronização. Aí, portas, use cases e mappers explícitos pagam.
Mas para a maioria das telas — CRUD, listagens, formulários guiados pelo backend — a camada completa é over-engineering. O sinal de alarme: mais arquivos de mapeamento e interface do que lógica de fato. A régua prática, herdada de Frontend: isole o que tem regra e valor; não burocratize o que é montagem de tela. Comece extraindo a porta do API client (baixo custo, alto retorno em testabilidade) e só suba de nível quando a complexidade justificar.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”fetch/axiosdentro do componente de UI. Acopla renderização à rede, impede injeção de fake e viola SRP e DIP. Esconda atrás de uma porta.- “God component”. Componente de centenas de linhas que busca, calcula e renderiza — o controller-que-faz-tudo do frontend.
- Regra de negócio no JSX. Cálculo de desconto/elegibilidade no corpo do render pertence a um use case/hook, não à camada de UI.
- Context como banco de dados global. Injeção de dependência (repos, config) é bom uso; jogar todo o estado mutável da app num único Context causa re-renders em cascata — prefira uma store com seletores (Observer).
- Clean Architecture completa em tela CRUD. Indireção sem retorno; o inverso do God component, igualmente nocivo.
- Confundir framework com arquitetura. React é detalhe de infra da camada de UI; a lógica de domínio não deveria depender dele.
Relações
Seção intitulada “Relações”- Área: Frontend
- Fundamentos: Arquitetura Hexagonal, Ports, Adapters, Clean Architecture, Use Cases
- Princípios: SOLID, SRP - Single Responsibility Principle, OCP - Open-Closed Principle, DIP - Dependency Inversion Principle
- Padrões: Design Patterns (GoF), Strategy, Observer
- Habilita: Testabilidade e Arquitetura e Testes e TDD no Frontend
- Rede: APIs - REST, GraphQL e gRPC
- Visão integrada: Como Tudo se Conecta
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Por que dizer que um API client é “um adapter atrás de uma porta” e qual benefício isso traz?
Resposta
Porque o componente/hook depende de uma interface (a porta, ex. UserRepository), e a implementação HTTP é apenas um adapter que a satisfaz. Isso é DIP: alto e baixo nível dependem da abstração. O benefício direto é testabilidade — injeta-se um fake em memória, sem rede, tornando o teste rápido e determinístico.
2. Quais são as três camadas do frontend hexagonal e qual a regra de dependência entre elas?
Resposta
Domínio/lógica (hooks, use cases, stores), UI (componentes) e infraestrutura (API clients, storage, SDKs). A regra de dependência: UI e domínio dependem de abstrações/portas; a infraestrutura implementa essas portas. O domínio nunca importa detalhes concretos como axios.
3. Aponte o padrão GoF por trás de: (a) Redux/Zustand e (b) uma tabela cujo algoritmo de ordenação é passado por prop.
Resposta
(a) Observer — a store é o subject e os componentes que assinam via seletor são observers que re-renderizam na mudança de estado. (b) Strategy — o algoritmo é injetado como função, permitindo trocar comportamento sem modificar o componente (OCP).
4. Quando aplicar Clean Architecture completa no front é overkill, e qual o passo de menor custo para começar a arquitetar?
Resposta
É overkill em telas majoritariamente de “montagem” — CRUD, listagens, formulários guiados pelo backend — onde surgem mais arquivos de mapeamento/interface do que lógica real. O passo de menor custo e maior retorno é extrair a porta do API client (repository), o que já desacopla a rede e habilita testes com fake; sobe-se de nível só quando a regra de negócio no cliente justificar.