Ir al contenido

Wu y Backend-for-Frontend

Respuesta corta: Wu no es un Backend-for-Frontend (BFF), y no intenta serlo. Es la capa que compone y renderiza la UI. El BFF es la capa que obtiene los datos. Encajan bien, y saber dónde cae la línea te ahorra esperar cosas que Wu nunca hará.

wu-framework/server contiene cero llamadas de red. No hace fetch, no cachea respuestas, no agrega servicios y no gestiona sesiones. Todo lo que una app necesita para renderizar se lo pasas como props:

const cart = await renderApp('cart', {
module: () => import('./apps/cart/server.js'),
props: { items }, // ← esto lo obtuviste tú, no Wu
});

Es una frontera deliberada. La obtención de datos es donde se filtran las opiniones de cada framework: claves de caché, revalidación, propagación de autenticación, semántica de errores. Wu se mantiene fuera para poder seguir siendo una capa de composición que funciona bajo Next, Astro, Express, Fastify o Nest sin discutir con ninguno.

Así que Wu no te da:

  • cargadores de datos con alcance de petición
  • caché ni revalidación del lado del servidor
  • agregación de APIs ni fan-out
  • manejo de sesiones o tokens
  • autenticación a nivel de petición (su RBAC es del lado del cliente, por diseño)

El BFF es el proceso. Wu corre dentro de él, justo al final de la petición:

Navegador
│ GET /dashboard
┌─ BFF (tu proceso Node — ruta Next, Express, Fastify…) ─────────────┐
│ │
│ 1. Autentica la petición ← tuyo (cookie, JWT, sesión) │
│ 2. Fan-out a los servicios ← tuyo (orders, billing, CRM) │
│ 3. Da forma al payload de ESTA UI ← tuyo (es el trabajo del BFF) │
│ │
│ 4. Compón la página ← Wu │
│ const { byApp, state } = await renderApps([...]) │
│ │
│ 5. Envía HTML + script de estado ← helpers de Wu │
└─────────────────────────────────────────────────────────────────────┘

En concreto, con Express:

app.get('/dashboard', requireSession, async (req, res) => {
// ── Trabajo del BFF: tuyo ────────────────────────────────────────
const [orders, kpis] = await Promise.all([
ordersService.recent(req.user.id),
analyticsService.kpis(req.user.id),
]);
// ── Composición: Wu ──────────────────────────────────────────────
const { byApp, state, errors } = await renderApps([
{ name: 'orders', module: () => import('./apps/orders/server.js'),
props: { orders } },
{ name: 'analytics', module: () => import('./apps/analytics/server.js'),
props: { kpis } },
]);
for (const { app, error } of errors) log.warn({ app, error }, 'ssr failed');
res.send(template
.replace('<!--orders-->', byApp.orders)
.replace('<!--analytics-->', byApp.analytics)
.replace('<!--state-->', renderStateScript(state)));
});

La sesión nunca llega al navegador como estado legible por JavaScript: te autenticas en el paso 1 y solo viaja el resultado renderizado. Esa es la misma propiedad que te da el patrón de cookie httpOnly, y Wu no la debilita: simplemente no es el componente que la provee.

El patrón interesante: un BFF por micro-app

Sección titulada «El patrón interesante: un BFF por micro-app»

Los micro-frontends suelen venir con una división equivalente en el servidor: cada equipo es dueño de su propio BFF. Wu soporta esa forma sin maquinaria extra, porque el módulo de servidor de cada app es solo un módulo. Puede llamar al BFF de su propio equipo:

// apps/orders/server.js — propiedad del equipo de Orders
import { renderReact } from 'wu-framework/server';
import OrdersPanel from './OrdersPanel.jsx';
export default {
async render(props) {
// Esta app habla con SU backend. El shell nunca supo de él.
const orders = await fetch(`${process.env.ORDERS_BFF}/recent`, {
headers: { authorization: props.authHeader },
}).then((r) => r.json());
return renderReact(OrdersPanel).render({ ...props, orders });
},
};

El shell entonces pasa solo lo que la app necesita para autenticarse, y se queda ignorante de dónde vinieron los datos:

await renderApp('orders', {
module: () => import('./apps/orders/server.js'),
props: { authHeader: req.headers.authorization },
});

Si tu BFF es Next, la división es aún más limpia: obtén los datos en el Server Component y entrega el resultado a WuAppSSR:

// app/dashboard/page.jsx — Server Component
import { WuAppSSR } from '@wu-framework/next';
import { getSession } from '@/lib/session';
export default async function Dashboard() {
const session = await getSession(); // asunto del BFF
const orders = await ordersService.recent(session.userId);
return (
<WuAppSSR
name="orders"
module={() => import('@/apps/orders/server.js')}
props={{ orders }} // asunto de composición
/>
);
}

Next se encarga del enrutado, el caché y el ciclo de vida de la petición. Wu se encarga de renderizar la micro-app dentro de un shadow root encapsulado y de hidratarla. Ninguno de los dos necesita saber mucho del otro.

Cuándo Wu ayuda de verdad a una arquitectura BFF

Sección titulada «Cuándo Wu ayuda de verdad a una arquitectura BFF»

La razón para combinarlos es que un BFF resuelve el fan-out de datos pero deja sin resolver el fan-out de UI. Varios equipos siguen teniendo que poner marcado en la misma página. Esa es la mitad de Wu:

Problema Lo resuelve
Cada equipo necesita su propia forma de API BFF
Autenticación, sesión y secretos se quedan en el servidor BFF
Cada equipo publica su UI de forma independiente Wu
Su CSS no debe colisionar Wu (Shadow DOM)
Usan frameworks distintos Wu (13 adaptadores)
La página debe renderizar antes del JS Wu (SSR)
Deben intercambiar datos en el navegador Wu (bus, store, contratos)

Si quieres el panorama completo tal como suele verse un shell de producción:

  • Sesión y autenticación: una comprobación de cookie o token antes de renderizar nada. El RBAC de Wu es una capa de UX encima de eso, no un reemplazo: la barrera real es no servir el bundle.
  • Timeouts y fallbacks en cada llamada aguas arriba.
  • Caché: caché HTTP en la respuesta del shell, o un caché delante de tus servicios. Wu cachea manifiestos del lado del cliente, nada más.
  • Observabilidad: renderApps te da errors por app; regístralo con el nombre de la app para que un micro-frontend lento o roto sea atribuible.