Software Architecture Lab
Voltar para conceitos
FrontendIntermediário14 min

Camadas no Frontend

Services, hooks e componentes formam camadas com responsabilidades distintas. Entender onde cada coisa mora evita que lógica de negócio se espalhe por toda a aplicação.

Resumo

Arquitetura em camadas não é exclusividade do backend. Um frontend bem estruturado também tem zonas claras de responsabilidade: onde os dados são buscados e transformados, onde a lógica de UI vive, e onde os elementos visuais são renderizados.

Sem essas fronteiras, a lógica de negócio migra gradualmente para os componentes — e o código que deveria descrever a interface passa a carregar regras, validações e chamadas de API lado a lado com o JSX.

As três camadas

Camada de Serviço

Responsável por comunicação com o mundo externo: APIs, localStorage, cookies, WebSockets. Não sabe nada sobre React — são funções puras ou classes que recebem e retornam dados.

// src/features/users/userService.ts
export async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("Usuário não encontrado");
  return res.json();
}
 
export async function updateUserName(
  id: string,
  name: string
): Promise<void> {
  await fetch(`/api/users/${id}`, {
    method: "PATCH",
    body: JSON.stringify({ name }),
  });
}

Sem React, sem JSX, sem hooks. Testável com um simples fetch mockado.

Camada de Lógica (Hooks)

Responsável por orquestrar o serviço e gerenciar o estado reativo. Usa os serviços para buscar dados, expõe estado e ações para os componentes, mas não renderiza nada.

// src/features/users/useUser.ts
function useUser(id: string) {
  const [user, setUser] = useState<User | null>(null);
  const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
 
  useEffect(() => {
    setStatus("loading");
    fetchUser(id)
      .then(data => { setUser(data); setStatus("idle"); })
      .catch(() => setStatus("error"));
  }, [id]);
 
  const rename = async (name: string) => {
    await updateUserName(id, name);
    setUser(prev => prev ? { ...prev, name } : prev);
  };
 
  return { user, status, rename };
}

O hook coordena a camada de serviço e expõe uma API limpa para o componente. Não importa de onde os dados vêm — o componente não sabe.

Camada de UI (Componentes)

Responsável por renderizar e capturar interações. Recebe dados e callbacks via props, sem saber como os dados foram buscados ou de onde vieram.

// src/features/users/UserProfile.tsx
function UserProfile({ userId }: { userId: string }) {
  const { user, status, rename } = useUser(userId);
 
  if (status === "loading") return <Skeleton />;
  if (status === "error") return <ErrorMessage />;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => rename("Novo Nome")}>
        Editar
      </button>
    </div>
  );
}

Por que essa separação importa

Tudo no componente
function OrderList() {
  const [orders, setOrders] = useState([]);
 
  useEffect(() => {
    fetch("/api/orders")
      .then(r => r.json())
      .then(data =>
        // lógica de negócio dentro do componente:
        setOrders(data.filter(o => o.status !== "cancelled")
          .map(o => ({
            ...o,
            total: o.items.reduce((s, i) => s + i.price, 0),
          }))
        )
      );
  }, []);
 
  return orders.map(o => (
    <div key={o.id}>
      {o.id} — R$ {o.total.toFixed(2)}
    </div>
  ));
}
Cada responsabilidade no lugar certo
// orderService.ts
export async function fetchActiveOrders(): Promise<Order[]> {
  const res = await fetch("/api/orders");
  const data = await res.json();
  return data.filter(o => o.status !== "cancelled");
}
 
// useOrders.ts
function useOrders() {
  const [orders, setOrders] = useState<Order[]>([]);
  useEffect(() => {
    fetchActiveOrders().then(setOrders);
  }, []);
  const withTotal = orders.map(o => ({
    ...o,
    total: o.items.reduce((s, i) => s + i.price, 0),
  }));
  return { orders: withTotal };
}
 
// OrderList.tsx
function OrderList() {
  const { orders } = useOrders();
  return orders.map(o => (
    <div key={o.id}>
      {o.id} — R$ {o.total.toFixed(2)}
    </div>
  ));
}

Visualização orbital das camadas

As três camadas formam órbitas ao redor do núcleo da aplicação. Camadas internas não conhecem as externas — as dependências sempre fluem para dentro.

Camadas do Frontend — Diagrama Orbital◎ Diagrama Orbital
Regras de NegócioValidaçõesHTTPCLIWebSocketBanco de DadosAPIs ExternasE-mailFeature / Domínio
CentroCasos de UsoControllers / PresentersInfrastructure

Dependências fluem para o centro · Passe o mouse para destacar

Sinais de que as camadas estão misturadas

  • fetch diretamente dentro de componentes com JSX.
  • Lógica de transformação de dados (filter, map, cálculos) dentro do return do componente.
  • Hooks que retornam JSX.
  • Funções de serviço que importam useState ou useEffect.

Conceitos relacionados

Conteúdos relacionados