Disponível para freelancerEntre em contato para que eu possa ajudar seu negócio a crescer ou tirar sua ideia do papel!

Estou interessado
Arquitetura de Camada de API para Aplicações Frontend

Arquitetura de Camada de API para Aplicações Frontend

Abre qualquer codebase React de tamanho médio e procura por fetch ou axios.get. Se você encontrar essas chamadas espalhadas em uma dúzia de componentes, você já tem o bug que este post explica. O endpoint muda, o formato da resposta muda, ou o time de backend renomeia um campo — e agora você está dando grep na aplicação inteira em vez de editar um arquivo só.

Isso não é um problema de ferramenta. É uma camada que falta.

A camada que ninguém desenha de propósito

A maioria das aplicações frontend faz sua camada de fetching crescer de forma orgânica. Um componente precisa de dado, então alguém adiciona um useEffect com uma chamada fetch. Funciona, então o próximo componente copia o padrão. Seis meses depois você tem:

  • O mesmo endpoint chamado a partir de quatro componentes diferentes, cada um com um tratamento de erro ligeiramente diferente
  • Formatos de resposta da API vazando direto para as props dos componentes
  • Nenhum lugar único para adicionar headers de autenticação, retry ou logging
  • Componentes impossíveis de testar porque testá-los significa mockar fetch

A solução não é uma biblioteca. É um limite. Você precisa de uma camada que fica entre "como o dado é buscado" e "como o componente usa o dado", de forma que uma mudança de um lado nunca toque no outro.

Eu estruturo isso em três peças: adapters, services e hooks. Cada uma com um único trabalho.


As três peças

Adapters: falam com o formato da API, e só isso

O único trabalho de um adapter é transformar uma resposta HTTP crua em um formato que sua aplicação entende — e transformar o dado da sua aplicação no que a API espera receber de volta.

// adapters/user.adapter.ts
export type ApiUserDto = {
  id: string
  first_name: string
  last_name: string
  email_address: string
  created_at: string
}

export type User = {
  id: string
  fullName: string
  email: string
  createdAt: Date
}

export function toUser(dto: ApiUserDto): User {
  return {
    id: dto.id,
    fullName: `${dto.first_name} ${dto.last_name}`,
    email: dto.email_address,
    createdAt: new Date(dto.created_at),
  }
}

Repara na nomenclatura. ApiUserDto é feio de propósito — ele espelha exatamente o que o backend manda, com snake_case e tudo mais. User é o tipo com o qual sua aplicação de fato trabalha. O adapter é o único arquivo que conhece os dois formatos.

Services: donos do endpoint, não do componente

Um service encapsula as chamadas HTTP de um recurso e retorna o dado já adaptado. Ele conhece a URL, o método e qual adapter aplicar — nada além disso.

// services/user.service.ts
import { toUser, type ApiUserDto, type User } from '@/adapters/user.adapter'
import { httpClient } from '@/lib/http-client'

export const userService = {
  async getById(id: string): Promise<User> {
    const dto = await httpClient.get<ApiUserDto>(`/users/${id}`)
    return toUser(dto)
  },

  async update(id: string, changes: Partial<User>): Promise<User> {
    const dto = await httpClient.patch<ApiUserDto>(`/users/${id}`, changes)
    return toUser(dto)
  },
}

httpClient aqui é um wrapper fino em cima do fetch que cuida da base URL, dos headers de autenticação e do parsing de erro uma única vez. Se seu backend trocar de REST para GraphQL amanhã, essa é a única camada que muda.

Hooks: conectam services aos componentes

O hook é onde vivem as preocupações específicas do React — estado de loading, cache, refetch. Ele chama o service; nunca chama fetch diretamente.

// hooks/use-user.ts
import { useQuery } from '@tanstack/react-query'
import { userService } from '@/services/user.service'

export function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => userService.getById(id),
  })
}

O componente nunca importa httpClient, nunca vê ApiUserDto, e nunca sabe que o endpoint é /users/${id}:

// components/UserProfile.tsx
import { useUser } from '@/hooks/use-user'

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId)

  if (isLoading) return <Spinner />
  return <h1>{user.fullName}</h1>
}

Por que essa separação realmente importa

Aqui está o cenário que vende essa arquitetura: o time de backend renomeia email_address para email e reestrutura created_at dentro de um objeto aninhado metadata.

Sem a camada, você fica editando cada componente que toca em dado de usuário, na esperança de ter encontrado todos.

Com a camada, você abre adapters/user.adapter.ts, atualiza o toUser, e pronto:

// ✅ Só o adapter muda
export function toUser(dto: ApiUserDto): User {
  return {
    id: dto.id,
    fullName: `${dto.first_name} ${dto.last_name}`,
    email: dto.email, // antes era dto.email_address
    createdAt: new Date(dto.metadata.created_at), // antes era dto.created_at
  }
}

Cada hook, cada componente, cada teste que usa User continua funcionando sem precisar de nenhuma mudança. É esse o ponto todo — o limite absorve a mudança em vez de propagá-la.


Erros comuns

  • Pular o adapter "porque a API já está limpa". APIs limpas hoje ganham um campo novo, uma chave renomeada, ou um endpoint v2 amanhã. O adapter custa cinco minutos agora e economiza horas depois.
  • Colocar chamadas de fetch direto dentro de custom hooks. Isso acopla sua camada de React à sua camada de HTTP. Você não consegue reaproveitar a lógica de fetch fora de um componente, nem testá-la sem renderizar um.
  • Retornar a resposta crua do Axios/fetch a partir de um service. Se um service retorna AxiosResponse<T> em vez de T, todo mundo que chama precisa saber que existe um .data. Desembrulhe isso dentro do service.
  • Um api.ts gigante com todos os endpoints. Isso vira um gargalo de import e um ímã de conflito de merge. Separe os services por recurso — user.service.ts, orders.service.ts — do mesmo jeito que você separaria módulos de domínio no backend.

Quando usar / quando pular

SituaçãoRecomendação
Aplicação pequena, uma ou duas chamadas de API no totalPule. Um useQuery com um fetch inline resolve.
Vários componentes consumindo o mesmo recursoUse o padrão completo. Você vai buscar o mesmo dado de mais lugares do que imagina.
API do backend instável ou ainda em evoluçãoUse. O adapter é seu seguro contra as mudanças.
Você está construindo um design system ou uma biblioteca de componentes compartilhadaUse. Componentes nunca deveriam assumir um formato específico de backend.

O que fazer agora

Escolha um recurso da sua aplicação — usuários, pedidos, o que for buscado a partir de mais lugares — e separe ele nesses três arquivos. Não tenta refatorar tudo de uma vez; é assim que refatorações morrem pela metade.

Depois que você fizer isso para um recurso, o padrão se repete sozinho. O próximo endpoint que você adicionar vai naturalmente seguir essa mesma estrutura de três arquivos, e daqui a seis meses, uma renomeação do backend vai custar cinco minutos em vez de uma tarde inteira de grep.