Inicio / Artículos

Convierte los controladores de rutas de Next.js en una capa BFF deliberada

Aprenda qué soluciona el patrón Backend for Frontend, por qué vuelve a utilizarse en las aplicaciones Next.js y cómo evitar que los manejadores de rutas se conviertan en objetos gigantescos e inmanejables.

6 min de lectura1435 palabras

Un desarrollador de un equipo móvil preguntó una vez por qué la pantalla de pago requería seis llamadas API separadas para renderizarse, siendo que tres de ellas devolvían campos superpuestos extraídos del mismo servicio subyacente. La explicación honesta fue que nadie era responsable de determinar qué necesitaba realmente la interfaz frontal, por lo que cada equipo enviaba su propia versión de la solución y dejaba al cliente que las combinara. Eso no es realmente un problema técnico, sino organizacional, y resulta ser exactamente lo que fue diseñado para abordar el patrón Backend for Frontend. Probablemente por eso este patrón ha vuelto a aparecer en varios artículos recientemente, después de haber estado durante años como una idea casi olvidada en los blogs técnicos de Netflix y SoundCloud hace una década.

BFF está lejos de ser una idea nueva. Muchos ingenieros ya lo estaban desarrollando bajo otro nombre antes de que “BFF” se convirtiera en el término estándar. Lo novedoso es que se vuelve a introducir en el contexto específico de las aplicaciones Next.js, que han evolucionado gradualmente hacia capas de agregación de API, a menudo sin que nadie lo decidiera intencionadamente. Dada esa tendencia, vale la pena adoptar este patrón de forma intencionada en lugar de encontrárselo por casualidad.

La naturaleza del problema

El escenario se repite una y otra vez en formas ligeramente diferentes. Cuenta con unos pocos servicios backend, algunos desarrollados internamente y otros API de proveedores añadidos posteriormente. También tiene múltiples clientes: una aplicación web, una aplicación móvil y quizás una herramienta de administración interna. Cada uno de estos clientes tiene requisitos diferentes. El cliente móvil desea una carga ligera, ya que a menudo funciona en una red poco fiable. El panel de control web necesita una respuesta completamente denormalizada con toda la información ya combinada, para evitar una cadena de solicitudes secuenciales. La herramienta de administración requiere datos en bruto, sin procesar, ya que quienes la utilizan suelen estar solucionando problemas.

Si se omite la capa BFF, solo quedan dos posibles resultados, ambos negativos. O bien los servicios principales se vuelven excesivamente complejos, convirtiéndose en endpoints de uso general que intentan satisfacer a todos los usuarios posibles al mismo tiempo, lo cual es básicamente un cementerio de soluciones del tipo “solo agrega otro parámetro de consulta opcional”, o bien cada cliente acaba reimplementando su propia lógica de agregación sobre los mismos servicios backend. Así es como terminan apareciendo tres definiciones ligeramente diferentes y erróneas de “usuario activo” dispersas en tres bases de código separadas.

Qué es realmente un BFF

Un backend para frontend es una capa de servidor ligera, generalmente una por tipo de cliente o familia de clientes, que se encuentra entre la interfaz de usuario y los servicios backend reales. Su única función es dar forma a los datos para el consumidor específico al que sirve. El BFF orientado a la web y el BFF orientado a móviles pueden llamar a servicios subyacentes idénticos, pero devolver estructuras de respuesta completamente diferentes, ya que cada uno resuelve un problema de presentación distinto.

Esta idea surge una y otra vez en las conversaciones sobre Next.js porque el App Router, si se utiliza de esa manera, proporciona esencialmente una capa BFF de forma gratuita. Los controladores de rutas y los componentes del servidor ya se ejecutan en el servidor, ya se encuentran entre el cliente y el backend, y ya tienen la capacidad de enviar múltiples solicitudes en paralelo y reorganizar el resultado combinado antes de que llegue al navegador. Muchos equipos que adoptaron el App Router ya están realizando tareas al estilo BFF sin nombrarlas formalmente ni aplicarlas de manera consistente.

// app/api/checkout-summary/route.ts
// A web-specific BFF endpoint that aggregates three backend calls
// into the one shape the checkout screen actually needs
export async function GET(req: Request) {
  const userId = getUserIdFromSession(req);  const [cart, pricing, shippingOptions] = await Promise.all([
    fetch(`${CART_SERVICE}/users/${userId}/cart`).then(r => r.json()),
    fetch(`${PRICING_SERVICE}/quote?userId=${userId}`).then(r => r.json()),
    fetch(`${SHIPPING_SERVICE}/options?userId=${userId}`).then(r => r.json()),
  ]);  // Reshape into exactly what the checkout component renders,
  // nothing more. This is the whole point.
  return Response.json({
    items: cart.items.map((i: CartItem) => ({ id: i.id, name: i.name, qty: i.qty })),
    total: pricing.total,
    currency: pricing.currency,
    shipping: shippingOptions.filter((o: ShippingOption) => o.available),
  });
}

Imagínese ahora que la aplicación móvil se conecta directamente a esos mismos tres servicios y gestiona su propia agregación en Swift o Kotlin, aplicando de forma independiente la lógica para determinar qué opciones de envío están disponibles. Así es precisamente como surge la desviación. Dos equipos, dos lenguajes, dos interpretaciones distintas de la misma regla de negocio, y tarde o temprano esas interpretaciones difieren de tal manera que nadie se da cuenta hasta que llega un ticket de soporte a alguien.

Donde esto se vuelve político, no solo técnico

La parte más difícil de adoptar BFF de la manera correcta tiene poco que ver con escribir el código. Se trata de quién es el responsable de él. Un BFF creado únicamente para el equipo web solo funcionará si hay alguien con autoridad para modificarlo rápidamente a cargo, idealmente el propio equipo web, y no un grupo de plataforma compartida atado a un calendario de lanzamientos de dos semanas. Si el layer BFF acaba en manos de un equipo de backend separado que lo trata como otro microservicio sujeto al mismo proceso de aprobación que todo lo demás, no se ha resuelto el cuello de botella original; simplemente se ha desplazado un paso más cerca del cliente.

Esta es también la razón por la cual este patrón encaja de manera tan natural en una base de código Next.js: los desarrolladores que escriben los controladores de ruta suelen ser los mismos que crean los componentes que los invocan. No hay tickets que presentar, ni transferencia de tareas entre equipos, ni pipeline de despliegue separado que coordinar. Si necesitas cambiar la estructura de la respuesta del resumen de pago, realizas ese cambio en la misma solicitud de integración que el componente que la muestra.

La trampa: convertir cada controlador de ruta en un objeto divino

Existe un modo de fallo en el otro extremo del espectro que rara vez se menciona. Una vez que un equipo descubre que los manejadores de rutas pueden agrupar lo que deseen, resulta difícil resistirse a la tentación de crear un único endpoint gigante que devuelva todo el estado inicial de la aplicación en una sola llamada, ya que menos viajes de ida y vuelta parecen ser una ventaja obvia. A veces lo son. Pero también significa que un único manejador de rutas termina soportando seis funcionalidades no relacionadas al mismo tiempo, y un pequeño cambio en el módulo de envíos ahora obliga a colocarlo en el mismo archivo que el widget de puntos de fidelidad, porque ambos se han incluido en algo como getInitialAppState.

El patrón sigue siendo válido cuando un endpoint BFF corresponde a una sola pantalla o a una función con un alcance bien definido, y no a “todo lo que la aplicación podría necesitar al cargarse”. Si le resulta difícil nombrar el endpoint según una parte específica de la interfaz de usuario, eso suele ser señal de que su alcance se ha vuelto demasiado amplio.

También hay una señal de advertencia relacionada que merece atención: los endpoints BFF que pasan de transformar datos a reimplementar la lógica empresarial. Un manejador de rutas que decide por sí mismo si un cliente califica para envío gratuito, en lugar de simplemente consultar al servicio de precios y transmitir la respuesta, se ha convertido silenciosamente en una segunda fuente de verdad para una regla que ya existe en otra parte del sistema. La función de un BFF es traducir y combinar datos, no tomar decisiones. En cuanto comienza a decidir cosas, volvemos al mismo problema de desviación mencionado anteriormente, solo que ahora está un nivel más cerca del cliente, donde es mucho más difícil detectarlo durante una revisión de código.

Qué he llegado a concluir al respecto

Un BFF no tiene por qué tratarse como un compromiso arquitectónico formal que incluya diagramas y una página en wiki, aunque no hay nada de malo en hacerlo si se ajusta al funcionamiento de su organización. Para la mayoría de los equipos que ya trabajan con Next.js, adoptarlo se trata menos de introducir algo nuevo y más bien de nombrar lo que ya existe: reconocer que sus controladores de rutas funcionan como BFF, dejar de fingir que son rutas API genéricas y estructurarlos según lo que realmente necesita el cliente en lugar de basarse en los límites de los servicios backend. El patrón en sí nunca fue la parte difícil. Lo complicado, y lo que la mayoría de los equipos posponen, es admitir que la lógica de agregación necesita un lugar adecuado y luego entregar ese rol al equipo frontend que realmente es responsable de ella, en lugar de esperar a que la pantalla de pago requiera seis llamadas separadas para renderizarse antes de que alguien se detenga a preguntar por qué.

Lecturas relacionadas

  • Construyendo UI de agente AI multietapa con Next.js y el AI SDK — Aprenda cómo diseñar una interfaz de agente AI lista para producción utilizando herramientas tipadas, bucles multietapa y componentes de UI generativos en tiempo real en Next.js.
  • Generar automáticamente un cliente API de Next.js seguro con tipos a partir de NestJS Swagger — Aprenda cómo eliminar tipos API duplicados utilizando NestJS Swagger y Orval para generar automáticamente ganchos React Query seguros con tipos para Next.js.