08 dic 2025·7 min de lectura

Capa de servicio frontend: separar las llamadas a la API de los componentes de UI

Aprende cómo una capa de servicio frontend saca la lógica de fetch de los componentes, estandariza errores y hace que los cambios sean más seguros y rápidos.

Capa de servicio frontend: separar las llamadas a la API de los componentes de UI

Por qué las llamadas a la API dentro de componentes se convierten en bugs

Un patrón común en React (o similares) es un componente de UI que lo hace todo: renderiza la pantalla, llama a fetch, construye cabeceras, parsea JSON, maneja códigos de estado y decide qué texto de error mostrar. Funciona para el primer endpoint, pero se vuelve un desastre conforme la app crece.

Cuando cada pantalla implementa su propia lógica de petición, duplicas pequeñas decisiones que deberían ser consistentes. Un componente reintenta en 401, otro cierra la sesión. Uno envía Content-Type: application/json, otro lo olvida. Uno trata una respuesta vacía como éxito, otro falla con await res.json().

Por eso importa una capa de servicio frontend. Te da un único lugar para definir cómo tu app habla con la API, en vez de redecidirlo en cada componente.

Otro problema es el acoplamiento oculto. La UI empieza a depender de detalles de los endpoints que deberían ser privados: URLs exactas, query params, forma de las cabeceras y formatos de respuesta. Más tarde, si el backend cambia user_id por id, no estás cambiando un solo sitio de llamada. Estás rastreando pantallas, modales y hooks esperando no haber pasado por alto ninguno.

Los síntomas suelen verse así:

  • Cabeceras que son dolorosas de actualizar (token de auth, versión de la app, id del tenant)
  • Mensajes de error aleatorios que varían por pantalla
  • Bugs de autenticación que solo ocurren en ciertos flujos
  • Estados de carga que se quedan atascados tras una excepción
  • Comportamiento de la API que “funciona en una página”

Un ejemplo pequeño: una pantalla de perfil y otra de facturación piden /me. Una añade la cabecera auth desde local storage, la otra usa un valor en memoria obsoleto. Los usuarios ven “Please log in” en facturación, pero perfil sigue cargando.

Si heredaste un prototipo generado por IA, esto es especialmente común: llamadas fetch dispersas que parecen bien en modo demo y luego fallan en producción cuando aparecen auth, formatos de error y casos límite reales.

Qué es una capa de servicio (en términos sencillos)

Una capa de servicio es un pequeño conjunto de archivos que se encarga de hablar con tu backend. En lugar de que cada componente construya su propia URL, cabeceras y llamada fetch, tu app hace peticiones a través de funciones compartidas como getUser(), updateProfile() o createInvoice().

No es un framework ni necesita nuevas dependencias. Piénsalo como módulos de JavaScript o TypeScript que se colocan entre tu UI y el backend. Puedes empezar con un solo archivo (por ejemplo, apiClient.ts) y añadir más a medida que la app crece (por ejemplo, authService.ts, billingService.ts).

El objetivo es simple: una manera consistente de llamar a las APIs y una manera consistente de manejar los resultados. Eso incluye las partes aburridas que causan la mayoría de los bugs: timeouts, cabeceras de auth faltantes, formas de respuesta inconsistentes y mensajes de error que cambian de una página a otra.

Los componentes también se simplifican. Dejan de preocuparse por detalles HTTP. Piden datos o acciones y renderizan según el resultado.

En lugar de este tipo de código en el 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");

Terminas con código UI que lee como el flujo de usuario:

const user = await userService.getUser(id);

Esa diferencia importa cuando necesitas cambiar algo globalmente (añadir una cabecera, manejar un nuevo formato de error, cambiar endpoints). Con una capa de servicio, lo cambias una vez, no en 15 componentes.

Qué pertenece a la UI y qué a la capa de servicio

Una regla simple: la UI decide qué mostrar, y la capa de servicio decide cómo hablar con el servidor. Cuando eso se mezcla, los componentes se vuelven más difíciles de leer y más frágiles.

En la UI, mantén el trabajo ligado a la pantalla: estado local (loading, data, error), desencadenar acciones al hacer clic o al cargar la página, y mostrar retroalimentación como toasts, errores inline y estados vacíos. El componente no debería preocuparse si la petición usa fetch, qué cabeceras necesita o cómo interpretar un error extraño del backend.

En la capa de servicio, pon las partes que deberían ser consistentes en todas partes:

  • Construir peticiones (URL, método, cabeceras, token de auth, body)
  • Parsear respuestas (JSON vs cuerpo vacío, códigos de estado)
  • Convertir fallos en un conjunto pequeño de tipos de error que la UI entienda
  • Devolver una forma de resultado predecible para que cada pantalla lo maneje igual

Decide pronto cómo representas los resultados. Una opción común es: el servicio devuelve { data } o { error }, y la UI maneja loading. Esto mantiene los servicios enfocados y la lógica UI predecible.

El naming también importa. Elige un estilo y mantenlo, por ejemplo userService.getProfile() (qué hace) o ordersApi.create() (qué recurso toca). Mezclar estilos hace el código más difícil de encontrar luego.

Un ejemplo concreto: si un formulario de inicio de sesión necesita mostrar “Wrong password” vs “Network error”, el servicio debería traducir respuestas crudas a INVALID_CREDENTIALS o NETWORK. La UI solo elige el mensaje correcto.

Paso a paso: refactorizar una llamada API sin romper la UI

Empieza pequeño. Escoge un endpoint que aparezca en más de un sitio, como “obtener el usuario actual” o “cargar proyectos”. Son ideales porque puedes comprobar el cambio revisando dos pantallas.

Supón que tienes fetch('/api/me') duplicado en un header y en una página de ajustes. Tu objetivo es mantener el comportamiento de la UI igual mientras mueves los detalles de red a la capa de servicio.

1) Mueve el fetch a una función de servicio

Crea un archivo como services/userService.ts (el nombre da igual, la consistencia sí importa).

// 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 };
}

Fíjate que la forma de retorno siempre es predecible: { ok: true, data } o { ok: false, error }. Esa decisión elimina muchas dudas sobre “¿qué debo comprobar aquí?”.

2) Reemplaza los bloques antiguos y conserva la salida UI igual

Actualiza cada componente para llamar a getMe() y conserva los mismos estados de loading, success y error que antes.

Un camino seguro para refactorizar:

  • Sustituye el fetch inline por await getMe()
  • Mapea las actualizaciones de estado antiguas al nuevo resultado (if (result.ok) setUser(result.data) else setError(result.error))
  • Mantén los mismos spinners, toasts y estados vacíos
  • Prueba ambos lugares que usan el endpoint
  • Solo entonces elimina el código fetch antiguo

Antes de pasar al siguiente endpoint, confirma que nada cambió para el usuario. Si heredas un prototipo, aquí es donde suelen salir inconsistencias ocultas (formas JSON mixtas o supuestos frágiles de auth).

Estandarizar cómo se construyen las peticiones

Stop the “works on one page” issue
If your app works in demo but fails in production, we can stabilize it quickly.

Cuando cada componente arma su propia petición, las pequeñas diferencias se acumulan: una llamada olvida la cabecera de auth, otra envía el content type equivocado, otra usa una base URL ligeramente distinta. Una capa de servicio lo arregla dándote una sola “puerta” a la API.

Empieza con un único wrapper de peticiones (un cliente API) que se encargue de los detalles aburridos. Los componentes solo deben pasar lo único que cambia: endpoint, método y cualquier dato.

Un buen constructor de peticiones suele manejar, en un solo lugar:

  • Base URL y cabeceras comunes (como Accept: application/json)
  • Tokens de auth (leer desde almacenamiento, adjuntar en la cabecera, refrescar si hace falta)
  • Timeouts e IDs de petición (para que las llamadas no se queden colgadas y puedas trazar problemas)
  • Query params (codificados de forma consistente)
  • Bodies JSON (stringify consistente, con el content type correcto)

Auth suele ser el mayor beneficio. En lugar de esparcir lógica de Authorization por la UI, que el cliente adjunte el token automáticamente. Si tu token puede expirar, mantén el refresco dentro del cliente también. Así, una pantalla de perfil y una de facturación se comportan igual, y un cambio de auth es una sola edición.

Sé estricto sobre cómo pasas params y bodies. Por ejemplo, decide que GET toma params y POST/PUT toman body, y que el cliente lo aplica. Esto evita el error común de “¿por qué el servidor recibe un body vacío?”.

Ejemplo concreto: un input de “Buscar usuarios” podría llamar searchUsers({ q, page }). La UI solo proporciona q y page. El cliente lo convierte en GET /users/search?q=...&page=..., añade cabeceras, adjunta auth, aplica timeout y añade un request ID. Si más tarde mueves la API a un nuevo dominio, solo cambia la base URL.

Estandarizar respuestas y manejo de errores

Cuando cada componente decide qué es “éxito”, la UI acaba llena de reglas pequeñas: a veces lee data, otras user, otras comprueba ok. Una capa de servicio funciona mejor cuando siempre devuelve la misma forma a la UI, así los componentes permanecen simples.

Normalizar respuestas exitosas

Elige un contrato para lo que la UI recibe. Las funciones de servicio deberían devolver el payload parseado directamente o un sobre consistente como { data, meta }. La mayoría de los equipos mantiene el código UI más limpio devolviendo solo el payload.

Sé estricto. Si un endpoint devuelve { user: {...} } y otro devuelve { data: {...} }, normalízalos dentro del servicio para que el componente siempre reciba el mismo tipo de valor.

Crear un único formato de error que la UI pueda mostrar

No lances cadenas aleatorias en un sitio y objetos Response en otro. Define un solo objeto de error que la UI pueda renderizar sin adivinar.

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

Luego mapea códigos de estado comunes en un solo lugar para que toda la app se comporte consistentemente:

  • 401: pedir al usuario que vuelva a iniciar sesión (kind: auth)
  • 403: mostrar “No tienes acceso” (kind: forbidden)
  • 404: mostrar “No encontrado” y parar reintentos (kind: not_found)
  • 429: mostrar “Demasiadas peticiones” y sugerir esperar (kind: rate_limited)
  • 500+: mostrar un fallback tranquilo y permitir reintento (kind: server)

Para depuración, registra contexto útil como estado, nombre del endpoint y un header de request id si lo tienes. No registres tokens ni payloads que puedan contener secretos. Los usuarios deben ver un mensaje amigable y los desarrolladores deben tener los detalles.

Reintentos, caching y cancelación sin ensuciar la UI

Una vez que las llamadas API viven en un solo lugar, puedes añadir mejoras sin tocar cada pantalla. La UI sigue centrada en estados de carga y renderizado, mientras la capa de servicio maneja las partes complejas.

Caching simple para evitar peticiones duplicadas

No todas las peticiones necesitan caching, pero un poco puede evitar molestias comunes como volver a pedir el mismo perfil de usuario al cambiar de pestaña. Un enfoque práctico es un pequeño cache en memoria con límite de tiempo corto (por ejemplo, 10 a 30 segundos) para lecturas que no cambian a menudo.

Ejemplo: tu dashboard y la página de ajustes piden /me. Si se montan cerca en el tiempo, puedes devolver el resultado cacheado en lugar de disparar dos peticiones y competir por las respuestas.

Reintentos, pero solo cuando sea seguro

Los reintentos deben ser la excepción, no la regla. Reintentar una petición de lectura (GET) tras un fallo de red suele estar bien. Reintentar una escritura (POST, PUT, DELETE) puede crear duplicados o cambios no deseados.

Mantén las reglas de reintento en la capa de servicio para que los componentes no inventen su propio comportamiento:

  • Reintentar solo en métodos seguros (normalmente GET) y solo en errores de red o respuestas 5xx.
  • Usa un límite pequeño (1 o 2 reintentos) y un retraso corto.
  • Nunca reintentes fallos de autenticación (401) automáticamente.

Cancelación para navegación rápida y búsquedas

Si un usuario escribe en una caja de búsqueda o navega rápido, las peticiones antiguas deberían detenerse. Si no, obtienes resultados obsoletos que parpadean en pantalla.

Usar AbortController en la capa de servicio mantiene la cancelación consistente:

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

Los componentes solo pasan un signal y olvidan la implementación. El resultado: menos condiciones de carrera, menos warnings sobre actualizaciones de estado tras un unmount y código UI más limpio.

Errores comunes a evitar

Rebuild the right way
If patching is risky, we can rebuild key parts into a maintainable, production-ready foundation.

Una capa de servicio debe simplificar la UI. La mayoría de los problemas ocurren cuando crece sin límites claros y la gente deja de confiar en ella.

Una trampa común es convertir la capa de servicio en un vertedero. Si tu “servicio” empieza a decidir qué botón debe desactivarse o cómo debe verse una pantalla, ya no es un servicio. Mantenlo enfocado en hablar con el servidor y dar forma a los datos en algo útil para la app.

Otro error es devolver objetos Response crudos a la UI. Eso obliga a cada componente a recordar cuándo llamar a json(), cómo comprobar ok y qué hacer con los códigos de estado. La UI debería recibir datos simples (o un error claro), no un objeto de red de bajo nivel.

Cuidado con la deriva de nombres y formas. Si una función devuelve { user }, otra devuelve { data: user } y una tercera devuelve user directamente, los bugs salen en tiempo de ejecución. Elige un patrón y mantenlo en todos los archivos.

Los errores son donde muchas apps se vuelven desordenadas. Si el servicio atrapa errores y devuelve null o un arreglo vacío “para ser seguro”, la UI no puede reaccionar correctamente. La UI necesita saber la diferencia entre “sin resultados” y “la petición falló”.

Finalmente, evita acoplar servicios a una sola pantalla. Si nombras funciones según páginas (por ejemplo getSettingsPageData) o metes supuestos de UI en parámetros, la reutilización empeora y los refactors se hacen más lentos.

Checklist rápido antes de mergear

Haz una pasada rápida por consistencia. Una capa de servicio solo paga si todos siguen las mismas reglas, incluso en cambios pequeños.

  • Los componentes UI llaman a una función de servicio, no a fetch ni a un cliente crudo.
  • Cada función de servicio tiene un contrato obvio: entrada clara y una forma de salida.
  • Los errores se traducen en un solo sitio a un pequeño conjunto de tipos o mensajes a nivel de app.
  • Detalles compartidos de las peticiones viven en un solo punto: base URL, cabeceras de auth, query params comunes, timeouts.
  • Nada sensible está hardcodeado en el frontend (tokens, API keys, credenciales temporales).

Una comprobación simple: abre un componente actualizado y pregúntate, “¿Podría cambiar el endpoint sin tocar este archivo UI?” Si la respuesta es no, probablemente la frontera está filtrando.

Ejemplo: limpiar un prototipo con fetch dispersos

Ship with confidence
We will get your AI-generated app ready for production, including refactoring and deployment prep.

Un patrón común en prototipos generados por IA (de herramientas como Lovable, Bolt, v0, Cursor o Replit) es el mismo bloque fetch copiado en muchos componentes. Una pantalla tiene una cabecera ligeramente distinta. Otra parsea JSON de otra forma. Una tercera muestra un toast por errores y las demás no hacen nada. Todo funciona en demo y luego se rompe cuando añades auth real, errores reales y usuarios reales.

En un prototipo, fetch estaba duplicado en 12 componentes. Los bugs eran pequeños pero constantes:

  • Deriva de cabeceras auth: algunas llamadas usaban Authorization, otras una cabecera custom, y algunas la olvidaban.
  • Parseo inconsistente: una llamada esperaba { data: ... }, otra usó JSON crudo, otra nunca comprobó res.ok.
  • Mensajes de usuario aleatorios: algunas pantallas mostraban “Something went wrong”, otras volcaban texto del servidor, otras no hacían nada.

El primer refactor fue intencionalmente pequeño. En lugar de reescribir la app, creamos un apiClient y dos servicios enfocados: authService (login, refresh, current user) y projectService (list, create, update).

Antes, un componente se veía así (simplificado):

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

Después, la UI solo pedía datos y manejaba loading:

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

La ganancia aparece rápido. La UI se acorta y las reglas viven en un solo sitio: cómo se construyen cabeceras, cómo se parsea JSON y cómo se forman los errores. Cuando el backend cambia (por ejemplo, empieza a devolver items en vez de data), lo arreglas una vez en la capa de servicio y todas las pantallas se actualizan.

Siguientes pasos: mantener la consistencia y pedir ayuda cuando haga falta

Una capa de servicio solo da beneficios si todos la usan. La forma más rápida de mantenerlos es hacerla la vía por defecto para cualquier trabajo nuevo con APIs. Si alguien necesita datos, debería buscar la función de servicio en vez de escribir un nuevo fetch dentro de un componente.

Escribe pruebas pequeñas para las funciones de servicio. No necesitas una suite grande. Quieres pruebas que confirmen que el happy path funciona y que los errores tienen la forma que la UI espera.

La documentación puede ser ligera, pero debe ser fácil de seguir. Una lista corta de nombres de funciones aprobadas evita duplicados como getUser, fetchUser y loadUser que hagan lo mismo con comportamientos ligeramente distintos.

Si estás lidiando con un codebase generado por IA que tiene fetch dispersos, auth inconsistente o problemas de seguridad (como secretos expuestos), FixMyMess (fixmymess.ai) puede ayudar. Se especializan en diagnosticar y reparar apps generadas por IA, incluyendo refactorizar capas de petición, endurecer la seguridad y preparar proyectos para producción.

Preguntas Frecuentes

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

Porque cada componente termina tomando decisiones ligeramente diferentes sobre cabeceras, parseo, reintentos y mensajes de error. Esas pequeñas diferencias generan fallos que solo aparecen en ciertas pantallas o flujos, especialmente alrededor de la autenticación y los casos límite.

What is a frontend service layer, in plain terms?

Una capa de servicio es un conjunto pequeño de funciones compartidas que se encargan de hablar con el backend, por ejemplo getMe() o createInvoice(). Los componentes llaman a esas funciones y se centran en el estado y en renderizar en lugar de en los detalles HTTP.

What belongs in the UI vs the service layer?

La UI debería encargarse del comportamiento de la pantalla: estados de carga, clics de botones y qué mensaje mostrar. La capa de servicio debe encargarse de construir peticiones, parsear respuestas y traducir fallos a una forma de error predecible que la UI pueda manejar.

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

Empieza por un endpoint que se use en más de un sitio, como /me o la lista de proyectos. Mueve el fetch a una función de servicio que siempre devuelva un resultado predecible, y luego cambia cada componente para que la use, manteniendo el mismo comportamiento visual.

What return shape should service functions use?

Usa una forma de retorno consistente en todas partes, por ejemplo { ok: true, data } y { ok: false, error, status }. La gran ventaja es que los componentes ya no adivinan qué deben comprobar, así los flujos de éxito y error son consistentes entre pantallas.

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

Crea un wrapper de peticiones (un cliente API) que controle la base URL, cabeceras comunes, adjuntar tokens de auth, codificar JSON y timeouts. Luego todas las funciones de servicio llaman a ese wrapper para que un cambio compartido se haga en un solo lugar.

How do I handle inconsistent backend response shapes?

Normaliza las respuestas en la capa de servicio para que la UI siempre reciba el mismo tipo de payload, aunque el backend devuelva formas distintas por endpoint. Así evitas que los componentes dependan de detalles como data vs user vs items.

What’s a good way to standardize error handling?

Define un único objeto de error a nivel de aplicación y mapea los fallos HTTP y de red a ese formato en un solo sitio. Así la UI puede mostrar el mensaje correcto sin adivinar, y evitas lanzar cadenas aleatorias o filtrar objetos Response de bajo nivel a los componentes.

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

Los reintentos suelen ser seguros solo para peticiones de lectura (como GET) y para errores de red o respuestas 5xx, con un límite bajo. La cancelación vale la pena para búsquedas y navegación rápida; mantener AbortController en la capa de servicio evita resultados obsoletos y actualizaciones de estado después del unmount.

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

Este patrón es muy común: bloques fetch copiados por componentes con deriva en cabeceras, parseo y supuestos de auth que solo funcionaban en modo demo. Si heredaste una app generada por IA, FixMyMess puede auditar el código gratis y refactorizar la capa de peticiones, arreglar bugs de auth y endurecer la seguridad para producción.