Una Arquitectura Monorepo Práctica con Next.js, Fastify, Prisma y NGINX

Monorepo de Plataforma: Next.js (Público + Admin) + API Fastify + DB Prisma + NGINX
Este documento describe la arquitectura objetivo actual para un monorepo con un sitio público Next.js, una aplicación de administración Next.js con sesiones de cookies, una API Fastify y un paquete de base de datos basado en Prisma. Está escrito para compañeros de equipo que trabajarán con múltiples paquetes y necesitan límites predecibles.
Restricciones que no estamos negociando (por ahora)
- El sitio público debe ser SSR (Next.js App Router). Sin atajos solo para SPA.
- El sitio público se comunica con el backend solo a través de rutas de API públicas bajo /api (por ejemplo, /api/content, /api/media).
- El administrador utiliza autenticación de sesión basada en cookies. Cookie solo HTTP. Sin tokens de localStorage.
- La API es Fastify. Sesiones a través de @fastify/session. Almacén “fakeRedis” para desarrollo; Redis en producción más adelante.
- El acceso a la base de datos es solo a través de @platform/db. Sin clientes Prisma directos dispersos en las aplicaciones.
- NGINX termina TLS y enruta /, /admin y /api. Un dominio está bien; el subdominio es opcional más adelante.
- El monitoreo existe desde el primer día (recopilación de Prometheus + marcadores de posición del panel de Grafana).
Diseño del repositorio
repo/
apps/
platform/ # public site (SSR)
admin/ # admin UI (cookie session)
packages/
api/ # Fastify backend (auth/users/content/media)
db/ # Prisma client + migrations
shared/ # shared types, UI bits, utilities
INFRASTRUCTURE/
nginx/
nginx.conf
sites/
app.conf
monitoring/
prometheus.yml
grafana/
docker-compose.yml
turbo.json
package.json
Compensaciones (las reales)
- Las sesiones de cookies son aburridas. Eso es una característica. Funcionan detrás de proxies y mantienen la lógica de autenticación fuera del frontend. La desventaja: debes configurar correctamente SameSite/Secure/path, o perseguirás errores de inicio de sesión “aleatorios”.
- Una superficie /api es limpia, pero también significa que debes ser estricto sobre lo que es público frente a lo que es solo para administradores. No filtres endpoints de administración solo porque sea conveniente.
- Prisma puede estar “listo para múltiples bases de datos” en el sentido de intercambiar proveedores. No es un puente mágico entre SQL y Mongo con un solo esquema. Si alguien dice que lo es, no lo ha implementado.
- Next.js SSR es bueno para el SEO y la primera carga. También puede sobrecargar la API si no configuras el almacenamiento en caché y la revalidación intencionalmente.
Contrato de enrutamiento (NGINX como puerta principal)
Por ahora, enrutamos todo a través de un solo dominio. Es más sencillo de depurar. Si más adelante movemos la administración a un subdominio, revisaremos el alcance de las cookies y las suposiciones de CSRF.
# INFRASTRUCTURE/nginx/sites/app.conf
server {
listen 80;
server_name yourdomain.com;
location /api/ {
proxy_pass http://api:4000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
location /admin/ {
proxy_pass http://admin:3001/;
proxy_set_header Host $host;
}
location / {
proxy_pass http://platform:3000/;
proxy_set_header Host $host;
}
}
Paquete API (Fastify) — sesiones y autenticación
La API es propietaria de la autenticación y el estado de la sesión. Las aplicaciones nunca emiten tokens. Simplemente devuelven cookies. Mantén la carga útil de la sesión pequeña. ID de usuario + rol. Eso es todo.
// packages/api/src/server.ts
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import session from "@fastify/session";
import { buildSessionStore } from "./sessionStore";
import { authRoutes } from "./routes/auth";
import { meRoutes } from "./routes/me";
export async function buildServer() {
const app = Fastify({ logger: true });
await app.register(cookie);
await app.register(session, {
secret: process.env.SESSION_SECRET || "dev-secret-change-me",
cookieName: "sid",
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/"
},
store: buildSessionStore(),
saveUninitialized: false
});
app.register(authRoutes, { prefix: "/api" });
app.register(meRoutes, { prefix: "/api" });
return app;
}
// packages/api/src/index.ts
import { buildServer } from "./server";
(async () => {
const app = await buildServer();
const port = Number(process.env.PORT || 4000);
await app.listen({ port, host: "0.0.0.0" });
})();
// packages/api/src/sessionStore.ts
import type { SessionStore } from "@fastify/session";
export function buildSessionStore(): SessionStore {
const mem = new Map<string, { value: any; expiresAt: number }>();
return {
get: (sid, cb) => {
const hit = mem.get(sid);
if (!hit) return cb(null, null);
if (Date.now() > hit.expiresAt) {
mem.delete(sid);
return cb(null, null);
}
cb(null, hit.value);
},
set: (sid, session, cb) => {
const ttlMs = 1000 * 60 * 60 * 8;
mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });
cb(null);
},
destroy: (sid, cb) => {
mem.delete(sid);
cb(null);
}
};
}
Paquete DB (Prisma) — cliente único, tipos compartidos
Exponemos un único PrismaClient desde @platform/db. La API importa eso y solo eso. Si creas un segundo cliente Prisma en una aplicación porque es “más rápido de prototipar”, solo estás creando un incidente futuro.
// packages/db/src/index.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
Aplicación de administración — sesión de cookies y guardia /me
La aplicación de administración es una aplicación normal de Next.js App Router. La única parte especial es que debe tratar la API como la fuente de verdad y siempre incluir cookies al llamar a /api/me.
// apps/admin/app/(admin)/layout.tsx
import { cookies } from "next/headers";
async function getMe() {
const cookieHeader = cookies().toString();
const res = await fetch(`${process.env.ADMIN_BASE_URL}/api/me`, {
headers: { cookie: cookieHeader },
cache: "no-store"
});
return res.json();
}
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const me = await getMe();
if (!me?.ok) {
return (
<html>
<body>
<p>No autorizado. Ve a /admin/login.</p>
</body>
</html>
);
}
return (
<html>
<body>{children}</body>
</html>
);
}
Paso a paso: flujo de inicio de sesión del administrador
- El navegador envía credenciales: POST /api/login
- NGINX reenvía /api/* a Fastify
- Fastify valida al usuario contra la DB (Prisma)
- Fastify escribe la sesión en el almacén fakeRedis (desarrollo) y devuelve Set-Cookie: sid=…
- El navegador almacena la cookie (solo HTTP)
- Los componentes del servidor de administración llaman a GET /api/me con la cookie adjunta
- El administrador renderiza el panel solo si /me devuelve ok=true
Una cosa que salió mal (para no repetirla)
Tuvimos un bucle de inicio de sesión a pesar de que /api/login devolvió 200. La cookie nunca se envió de vuelta en /api/me. La causa fue aburrida: ruta de la cookie + desajuste de enrutamiento del proxy. Configuramos la cookie para /admin por accidente, pero /api/me reside bajo /api. El navegador hizo exactamente lo que debía. No envió la cookie. La solución fue establecer la ruta de la cookie en "/" y confirmar que NGINX no estaba reescribiendo las rutas de una manera que lo rompiera.
Plan del proyecto (lanzar primero, refinar después)
- Fase 0: cableado del monorepo (Turbo, configuración de TS, importaciones de paquetes compartidos). Mantenerlo ligero.
- Fase 1: Endpoints de autenticación/sesión de la API: /login, /logout, /me. Añadir limitación de tasa básica en /login.
- Fase 2: MVP de administración: pantalla de inicio de sesión + guardia del lado del servidor + shell de panel mínimo.
- Fase 3: Endpoints de lectura de contenido para la plataforma pública: /api/content/* (solo lectura primero).
- Fase 4: CRUD de contenido en administración + carga de medios (empezar simple, luego cambiar el almacenamiento más tarde).
- Fase 5: Monitoreo y endurecimiento: endpoint de métricas de Prometheus, paneles de Grafana, encabezados, copias de seguridad.
Definición de terminado (para no discutir después)
- Un navegador nuevo puede iniciar sesión en /admin y permanecer conectado a través de las actualizaciones.
- La plataforma pública renderiza contenido SSR desde /api/content sin exponer endpoints solo para administradores.
- NGINX enruta /, /admin, /api correctamente en docker-compose.
- La API puede reiniciarse sin corromper el comportamiento de la sesión (los reinicios del almacén de desarrollo están bien, pero debe fallar elegantemente).
- Existe al menos un objetivo de raspado de Prometheus (incluso si los paneles de Grafana son marcadores de posición).
Si estás implementando un nuevo endpoint, decide primero: ¿es público, solo para administradores o interno? No adivines. Luego, ponlo detrás del prefijo correcto y protégelo. Ese es todo el juego.
Related Articles

OpenAI Agents API vs Agents SDK vs Responses API: ¿Sobre qué deberías construir en 2026?
El stack de agentes de OpenAI cambió en septiembre de 2026. Esta guía de arquitectura separa la Agents API, el Agents SDK, la Responses API y el Codex SDK por propiedad del runtime—para que los equipos puedan elegir el límite de control adecuado en lugar de comparar nombres de productos.

Arquitectura multi-base de datos con Prisma 7: Un Deep Dive para expertos
La gestión de paisajes de datos complejos requiere arquitecturas modernas. Prisma 7 ofrece funciones avanzadas para la integración multi-base de datos y aborda los desafíos de la persistencia políglota.

Arquitectura Multi-Inquilino de Grado Empresarial para una Plataforma Internacional
Loving Rocks es una plataforma de bodas de nivel empresarial diseñada con una verdadera arquitectura multiinquilino, bases de datos aisladas por inquilino e internacionalización integrada para escalabilidad global, seguridad y estabilidad operativa a largo plazo.