08 de dez. de 2025·7 min de leitura

Camada de serviço frontend: separar chamadas API dos componentes de UI

Aprenda como uma camada de serviço frontend tira a lógica de fetch dos componentes, padroniza erros e torna mudanças mais seguras e rápidas.

Camada de serviço frontend: separar chamadas API dos componentes de UI

Por que chamadas API dentro de componentes viram bugs

Um padrão comum em React (ou similares) é ter um componente de UI que faz tudo: renderiza a tela, chama fetch, monta cabeçalhos, faz parsing de JSON, trata códigos de status e decide qual texto de erro mostrar. Funciona para o primeiro endpoint e depois vira bagunça conforme a app cresce.

Quando cada tela implementa sua própria lógica de requisição, você duplica pequenas decisões que deveriam ser consistentes. Um componente tenta reautenticar no 401, outro desloga o usuário. Um envia Content-Type: application/json, outro esquece. Um trata uma resposta vazia como sucesso, outro quebra em await res.json().

É por isso que uma camada de serviço frontend importa. Ela te dá um lugar único para definir como sua app conversa com a API, em vez de redecidir isso em cada componente.

Outro problema é o acoplamento escondido. A UI começa a depender de detalhes do endpoint que deveriam ser privados: URLs exatas, query params, formato de cabeçalhos e formatos de resposta. Depois, se o backend muda de user_id para id, você não altera um único ponto. Você vai caçando telas, modais e hooks torcendo para não ter esquecido nenhum.

Os sintomas geralmente parecem com isso:

  • Cabeçalhos que são dolorosos para atualizar (token de auth, versão do app, tenant id)
  • Mensagens de erro aleatórias que mudam por tela
  • Bugs de autenticação que só acontecem em certos fluxos
  • Estados de loading que ficam presos após uma exceção
  • Comportamento da API que “funciona em uma página”

Um pequeno exemplo: uma tela de perfil e uma de cobrança ambas buscam /me. Uma adiciona o cabeçalho de auth do local storage, a outra usa um valor em memória obsoleto. Usuários veem “Please log in” na cobrança, mas o perfil ainda carrega.

Se você herdou um protótipo gerado por IA, isso é especialmente comum: fetch espalhados que parecem ok em modo demo e depois falham em produção quando auth, formatos de erro e casos de borda aparecem.

O que é uma camada de serviço (em termos simples)

Uma camada de serviço é um pequeno conjunto de arquivos que é responsável por conversar com seu backend. Em vez de cada componente montar sua própria URL, cabeçalhos e chamada fetch, sua app faz requisições à API por meio de funções compartilhadas como getUser(), updateProfile() ou createInvoice().

Não é um framework e não precisa de dependências novas. Pense nela como módulos JavaScript ou TypeScript simples que ficam entre sua UI e o backend. Você pode começar com um arquivo (apiClient.ts) e adicionar mais conforme a app cresce (authService.ts, billingService.ts).

O objetivo é simples: uma forma consistente de chamar APIs e uma forma consistente de tratar resultados. Isso inclui as partes chatas que causam a maioria dos bugs: timeouts, cabeçalhos de auth ausentes, formatos de resposta inconsistentes e mensagens de erro que mudam de página para página.

Os componentes também ficam mais simples. Eles deixam de se preocupar com detalhes HTTP. Pedem dados ou ações e renderizam com base no resultado.

Ao invés de este tipo de código no componente:

const res = await fetch(`/api/users/${id}`, {
  method: "GET",
  headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || "Failed");

Você termina com código de UI que lê como o fluxo do usuário:

const user = await userService.getUser(id);

Essa diferença importa muito quando você precisa mudar algo globalmente (adicionar um cabeçalho, tratar um novo formato de erro, trocar endpoints). Com camada de serviço você muda uma vez, não em 15 componentes.

O que pertence à UI vs à camada de serviço

Uma regra simples: a UI decide o que mostrar, e a camada de serviço decide como falar com o servidor. Quando isso se mistura, os componentes ficam mais difíceis de ler e mais fáceis de quebrar.

Na UI, mantenha trabalho atrelado à tela: estado local (loading, dados, erro), disparar ações em cliques ou na carga da página, e exibir feedback como toasts, erros inline e estados vazios. O componente não deveria se importar se a requisição usa fetch, quais cabeçalhos precisa ou como interpretar um erro estranho do backend.

Na camada de serviço, coloque as partes que devem ser consistentes em todo lugar:

  • Construção de requisições (URL, método, headers, token de auth, body)
  • Parsing de respostas (JSON vs corpo vazio, códigos de status)
  • Converter falhas em um pequeno conjunto de tipos de erro que sua UI entende
  • Retornar uma forma de resultado previsível para que cada tela trate do mesmo jeito

Decida cedo como representar resultados. Uma opção comum é: o serviço retorna ou { data } ou { error }, e a UI cuida do estado loading. Isso mantém os serviços focados e a lógica da UI previsível.

Nomear também importa. Escolha um estilo e mantenha, por exemplo userService.getProfile() (o que faz) ou ordersApi.create() (qual recurso atinge). Misturar estilos torna o código mais difícil de achar depois.

Um exemplo concreto: se um formulário de login precisa mostrar “Senha errada” vs “Erro de rede”, o serviço deve traduzir respostas brutas em INVALID_CREDENTIALS ou NETWORK. A UI então só escolhe a mensagem certa.

Passo a passo: refatorar uma chamada API sem quebrar a UI

Comece pequeno. Escolha um endpoint que apareça em mais de um lugar, como “obter usuário atual” ou “carregar projetos”. Esses são ideais porque você pode provar que a mudança funciona checando duas telas.

Suponha que você tenha fetch('/api/me') duplicado em um cabeçalho e em uma página de configurações. Seu objetivo é manter o comportamento da UI igual enquanto move os detalhes de rede para a camada de serviço.

1) Mova o fetch para uma função de serviço

Crie um arquivo como services/userService.ts (o nome não importa, seja consistente).

// services/userService.ts
export async function getMe() {
  const res = await fetch('/api/me', { credentials: 'include' });
  const data = await res.json().catch(() => null);

  if (!res.ok) {
    return { ok: false, error: data?.error || 'Request failed', status: res.status };
  }

  return { ok: true, data };
}

Note que a forma de retorno é sempre previsível: { ok: true, data } ou { ok: false, error }. Essa decisão única elimina muitas dúvidas do tipo “o que eu verifico aqui?” que geram bugs.

2) Substitua os blocos antigos e mantenha a saída da UI igual

Atualize cada componente para chamar getMe() e mantenha os mesmos estados de loading, sucesso e erro de antes.

Um caminho de refatoração seguro:

  • Troque o fetch inline por await getMe()
  • Mapeie as atualizações de estado antigas para o novo resultado (if (result.ok) setUser(result.data) else setError(result.error))
  • Mantenha os mesmos spinners, toasts e estados vazios
  • Teste os dois lugares que usam o endpoint
  • Só então delete o código fetch antigo

Antes de seguir para o próximo endpoint, confirme que nada mudou para o usuário. Se você está herdando um protótipo, é comum encontrar inconsistências ocultas aqui (formas JSON mistas ou suposições frágeis de auth).

Padronize como as requisições são construídas

Clean up duplicated fetch blocks
Refatoramos código spaghetti de API em serviços limpos sem alterar o comportamento da sua UI.

Quando cada componente constrói sua própria requisição, pequenas diferenças se acumulam: um chama esquece o cabeçalho de auth, outro envia o content type errado, outro usa uma base URL levemente diferente. Uma camada de serviço corrige isso dando uma única “porta” para a API.

Comece com um único request wrapper (um cliente API) que cuide dos detalhes chatos. Os componentes devem passar apenas o que é único: endpoint, método e quaisquer dados.

Um bom construtor de requisição geralmente trata, em um só lugar:

  • Base URL e cabeçalhos comuns (como Accept: application/json)
  • Tokens de auth (lidos do storage, anexados ao header, refresh opcional)
  • Timeouts e IDs de requisição (para que chamadas não fiquem penduradas e você possa rastrear problemas)
  • Query params (codificados de forma consistente)
  • Corpos JSON (stringify consistente, com content type correto)

Auth costuma ser o maior ganho. Em vez de espalhar lógica de Authorization pela UI, deixe o cliente anexar o token automaticamente. Se seu token expira, mantenha o comportamento de refresh dentro do cliente também. Assim, uma tela de perfil e uma de cobrança se comportam igual, e uma mudança de auth vira uma única edição.

Seja estrito sobre como passar params e bodies. Por exemplo, decida que GET aceita params e POST/PUT aceitam body, e o cliente aplica essa regra. Isso evita o bug comum “por que o servidor recebe um corpo vazio?”.

Exemplo concreto: um input “Search users” pode chamar searchUsers({ q, page }). A UI só fornece q e page. O cliente transforma isso em GET /users/search?q=...&page=..., adiciona cabeçalhos, anexa auth, aplica timeout e adiciona um request ID. Se depois você mover a API para outro domínio, só a base URL muda.

Padronize respostas e tratamento de erros

Quando cada componente decide o que é “sucesso”, a UI fica cheia de regras pequenas: às vezes lê data, às vezes user, às vezes verifica ok. Uma camada de serviço funciona melhor quando sempre retorna a mesma forma para a UI, assim os componentes ficam simples.

Normalizar respostas bem‑sucedidas

Escolha um contrato para o que a UI recebe. Funções de serviço devem ou retornar o payload já parseado diretamente, ou retornar um envelope consistente como { data, meta }. A maioria das equipes mantém o código da UI mais limpo retornando o payload direto.

Seja rígido nisso. Se um endpoint retorna { user: {...} } e outro { data: {...} }, normalize dentro do serviço para que o componente sempre receba o mesmo tipo de valor.

Crie um formato de erro que a UI possa exibir

Não lance strings aleatórias em um lugar e objetos Response em outro. Defina um único objeto de erro que a UI consiga renderizar sem adivinhar.

export type ApiError = {
  kind: "auth" | "forbidden" | "not_found" | "rate_limited" | "server" | "network" | "unknown";
  message: string;
  status?: number;
  requestId?: string;
};

Depois, mapeie códigos de status comuns em um só lugar para que o app inteiro se comporte consistentemente:

  • 401: pedir ao usuário para fazer login novamente (kind: auth)
  • 403: mostrar “Você não tem acesso” (kind: forbidden)
  • 404: mostrar “Não encontrado” e parar retries (kind: not_found)
  • 429: mostrar “Muitas requisições” e sugerir esperar (kind: rate_limited)
  • 500+: mostrar um fallback calmo e permitir retry (kind: server)

Para depuração, registre contexto útil como status, nome do endpoint e um header de request id se existir. Não registre tokens ou payloads que possam conter segredos. Usuários devem ver uma mensagem amigável, e desenvolvedores devem ter os detalhes.

Retries, cache e cancelamento sem poluir a UI

Quando chamadas de API vivem em um só lugar, você pode adicionar recursos de qualidade sem tocar cada tela. A UI fica focada em loading e renderização, enquanto o serviço lida com as partes bagunçadas.

Cache simples para evitar fetchs duplos

Nem toda requisição precisa de cache, mas um pouco pode evitar aborrecimentos como refazer fetch do mesmo perfil ao trocar de aba. Uma abordagem prática é um pequeno cache em memória com tempo curto (por exemplo, 10 a 30 segundos) para leituras que não mudam com frequência.

Exemplo: seu dashboard e a página de configurações pedem /me. Se eles montarem perto um do outro, você pode devolver o resultado em cache em vez de disparar duas requisições e competir por respostas.

Retries, mas só quando for seguro

Retries devem ser exceção, não padrão. Repetir uma requisição de leitura (GET) após um pico de rede costuma ser ok. Repetir uma escrita (POST, PUT, DELETE) pode criar duplicados ou alterações indesejadas.

Mantenha regras de retry na camada de serviço para que componentes não inventem seu próprio comportamento:

  • Retry apenas em métodos seguros (geralmente GET) e apenas em erros de rede ou respostas 5xx.
  • Use limite pequeno (1 a 2 retries) e atraso curto.
  • Nunca retry automaticamente falhas de autenticação (401).

Cancelamento para navegação rápida e busca

Se um usuário digita em um campo de busca ou navega rápido, requisições antigas devem parar. Caso contrário, resultados obsoletos podem piscar na tela.

Usar AbortController na camada de serviço mantém o cancelamento consistente:

export function searchUsers(query, { signal } = {}) {
  return api.get('/users/search', { params: { q: query }, signal });
}

Os componentes então só passam um signal e ignoram o encanamento. O resultado é menos condições de corrida, menos avisos sobre atualização de estado após unmount e código de UI mais limpo.

Erros comuns a evitar

Turn prototypes into production code
FixMyMess repara camadas de requisição geradas por IA para que sua UI pare de quebrar entre telas.

Uma camada de serviço deve simplificar a UI. A maioria dos problemas aparece quando ela cresce sem um limite claro e as pessoas param de confiar nela.

Uma armadilha comum é transformar a camada de serviço em um depósito de responsabilidade. Se seu “service” começa a decidir qual botão deve ficar desabilitado ou como uma tela deve parecer, ele deixou de ser um serviço. Mantenha‑o focado em falar com o servidor e moldar dados em algo que a app possa usar.

Outro erro é retornar objetos Response do fetch para a UI. Isso força cada componente a lembrar quando chamar json(), como checar ok e o que fazer com códigos de status. A UI deve receber dados simples (ou um erro claro), não um objeto de rede de baixo nível.

Fique atento ao desvio de nomes e formatos. Se uma função retorna { user }, outra { data: user } e uma terceira retorna user direto, bugs ficam invisíveis até runtime. Escolha um padrão e mantenha em todos os arquivos.

Erros são onde muitas apps ficam bagunçadas. Se o serviço captura erros e retorna null ou um array vazio “para ser seguro”, a UI não consegue reagir corretamente. Sua UI precisa saber a diferença entre “sem resultados” e “requisição falhou”.

Por fim, evite acoplar serviços a uma única tela. Se você nomeia funções por páginas (getSettingsPageData) ou coloca suposições de UI em parâmetros, o reuso fica difícil e refatorar vira lento.

Checklist rápido antes de fazer merge

Faça uma checagem rápida por consistência. Uma camada de serviço só vale a pena quando todo mundo segue as mesmas regras, mesmo em mudanças pequenas.

  • Componentes de UI chamam uma função de serviço, não fetch ou o cliente cru diretamente.
  • Cada função de serviço tem um contrato óbvio: entrada clara e uma forma de saída única.
  • Erros são traduzidos em um pequeno conjunto de tipos de erro ou mensagens em um só lugar.
  • Detalhes compartilhados de requisição vivem em um ponto: base URL, headers de auth, query params comuns, timeouts.
  • Nada sensível está hardcoded no frontend (tokens, chaves de API, credenciais temporárias).

Um teste simples: abra um componente atualizado e pergunte “Eu poderia trocar o endpoint sem tocar este arquivo de UI?” Se a resposta for não, o boundary provavelmente está vazando.

Exemplo: limpar um protótipo com fetchs espalhados

Ship with confidence
Deixamos sua app gerada por IA pronta para produção, incluindo refatoração e preparação para deploy.

Um padrão comum em protótipos gerados por IA (ferramentas como Lovable, Bolt, v0, Cursor ou Replit) é o mesmo bloco de fetch copiado em muitos componentes. Uma tela tem um header levemente diferente. Outra faz parsing diferente. Uma terceira mostra toast para erros, mas o resto falha silenciosamente. Tudo funciona em demo e então quebra quando você adiciona auth real, erros reais e usuários reais.

Em um protótipo, fetch estava duplicado em 12 componentes. Os bugs eram pequenos mas constantes:

  • Deriva de cabeçalho de auth: algumas chamadas usavam Authorization, outras um header customizado, algumas esqueciam.
  • Parsing inconsistente: uma chamada esperava { data: ... }, outra usava JSON cru, outra nunca checava res.ok.
  • Mensagens de usuário aleatórias: algumas telas mostravam “Something went wrong”, outras despejavam texto do servidor, outras não faziam nada.

A primeira refatoração foi propositalmente pequena. Em vez de reescrever a app, criamos um apiClient e dois serviços focados: authService (login, refresh, usuário atual) e projectService (list, create, update).

Antes, um componente parecia com isto (simplificado):

useEffect(() => {
  fetch('/api/projects', {
    headers: { Authorization: `Bearer ${token}` }
  })
    .then(r => r.json())
    .then(setProjects)
    .catch(() => toast('Error'));
}, [token]);

Depois, a UI apenas pedia dados e tratou o estado de loading:

useEffect(() => {
  projectService.list().then(setProjects).catch(showError);
}, []);

O ganho aparece rápido. A UI fica menor e as regras vivem em um lugar só: como cabeçalhos são montados, como JSON é parseado e como erros são modelados. Quando o backend muda (por exemplo, começa a retornar items em vez de data), você corrige uma vez na camada de serviço e todas as telas atualizam juntas.

Próximos passos: mantenha a consistência e peça apoio quando necessário

Uma camada de serviço só compensa se todo mundo a usar. A forma mais rápida de manter os benefícios é torná‑la o caminho padrão para qualquer trabalho novo com API. Se alguém precisa de dados, deve procurar a função de serviço, não escrever um novo fetch dentro do componente.

Escreva testes pequenos para funções de serviço. Você não precisa de uma suíte enorme. Quer provas de que o happy path funciona e que erros são moldados do jeito que a UI espera.

A documentação pode ser leve, mas precisa ser fácil de seguir. Uma lista curta de nomes aprovados para funções de serviço evita duplicados como getUser, fetchUser e loadUser fazendo a mesma coisa com comportamentos levemente diferentes.

Se você está lidando com uma base de código gerada por IA que tem fetch espalhados, auth inconsistente ou problemas de segurança (como segredos expostos), FixMyMess pode ajudar. Eles se concentram em diagnosticar e reparar apps geradas por IA, incluindo refatorar camadas de requisição, endurecer segurança e preparar projetos para produção.

Perguntas Frequentes

Why do API calls inside UI components cause so many bugs?

Porque cada componente acaba tomando decisões ligeiramente diferentes sobre cabeçalhos, parsing, retries e mensagens de erro. Essas pequenas diferenças geram bugs que só aparecem em telas ou fluxos específicos, especialmente em torno de autenticação e casos de borda.

What is a frontend service layer, in plain terms?

Uma camada de serviço é um pequeno conjunto de funções compartilhadas que cuidam de conversar com o backend, como getMe() ou createInvoice(). Os componentes chamam essas funções e ficam responsáveis pelo estado e pela renderização, em vez de detalhes HTTP.

What belongs in the UI vs the service layer?

A UI deve controlar comportamento da tela, como estado de carregamento, cliques de botão e qual mensagem mostrar. A camada de serviço deve construir requisições, fazer parsing das respostas e traduzir falhas em um formato previsível que a UI consiga tratar.

What’s the safest way to refactor one API call into a service without breaking the UI?

Comece por um endpoint usado em mais de um lugar, como /me ou a lista de projetos. Mova o fetch para uma função de serviço que sempre retorne um resultado previsível e então troque cada componente para usá‑la, mantendo a saída da UI igual enquanto testa os dois pontos de uso.

What return shape should service functions use?

Use uma forma consistente de retorno em todo lugar, como { ok: true, data } e { ok: false, error, status }. O grande benefício é que os componentes não precisam adivinhar o que checar, então fluxos de sucesso e erro permanecem consistentes entre telas.

How do I standardize headers, base URL, and auth handling?

Crie um wrapper de requisição único (um cliente API) que cuide de base URL, cabeçalhos comuns, anexar token de autenticação, codificação JSON e timeouts. Então todas as funções de serviço chamam esse wrapper, de modo que você altere o comportamento compartilhado em um só lugar.

How do I handle inconsistent backend response shapes?

Normalize as respostas na camada de serviço para que a UI receba sempre o mesmo tipo de payload, mesmo que o backend retorne formatos diferentes por endpoint. Isso evita que componentes codifiquem detalhes do backend como data vs user vs items.

What’s a good way to standardize error handling?

Defina um único objeto de erro a nível de app e mapeie falhas HTTP e de rede para ele em um só lugar. Assim a UI mostra a mensagem correta sem adivinhar e você evita lançar strings aleatórias ou vazar objetos Response de baixo nível para os componentes.

Should I add retries, caching, and request cancellation in the service layer?

Retries geralmente são seguros apenas para requisições de leitura como GET e apenas em erros de rede ou respostas 5xx, com limite pequeno. Cancelamento vale a pena para busca e navegação rápida; manter AbortController na camada de serviço evita resultados obsoletos e avisos sobre atualização de estado após unmount.

How does FixMyMess help if my AI-generated prototype has scattered fetch calls and auth bugs?

Esse padrão é extremamente comum: blocos de fetch copiados em vários componentes com deriva em cabeçalhos, parsing e suposições de autenticação que só funcionavam em modo demo. Se você herdou uma app gerada por IA, FixMyMess pode auditar o código e rapidamente refatorar a camada de requisição, corrigir bugs de auth e endurecer a segurança para produção.