Eine Praktische Monorepo-Architektur mit Next.js, Fastify, Prisma und NGINX

Illustration
Plattform-Monorepo: Next.js (Öffentlich + Admin) + Fastify API + Prisma DB + NGINX
Dieses Dokument beschreibt die aktuelle Zielarchitektur für ein Monorepo mit einer öffentlichen Next.js-Website, einer Next.js-Admin-App mit Cookie-Sessions, einer Fastify-API und einem Prisma-basierten DB-Paket. Es ist für Teammitglieder geschrieben, die mit mehreren Paketen arbeiten und vorhersehbare Grenzen benötigen.
Einschränkungen, die wir (vorerst) nicht verhandeln
- Die öffentliche Website muss SSR sein (Next.js App Router). Keine reinen SPA-Abkürzungen.
- Die öffentliche Website kommuniziert mit dem Backend nur über öffentliche API-Routen unter /api (z.B. /api/content, /api/media).
- Admin verwendet Cookie-basierte Session-Authentifizierung. HTTP-only Cookie. Keine localStorage-Tokens.
- API ist Fastify. Sessions über @fastify/session. „fakeRedis“-Speicher für die Entwicklung; Redis später in Produktion.
- DB-Zugriff erfolgt ausschließlich über @platform/db. Keine direkten Prisma-Clients, die in Apps verstreut sind.
- NGINX beendet TLS und leitet /, /admin und /api weiter. Eine Domain ist in Ordnung; Subdomain ist später optional.
- Monitoring existiert vom ersten Tag an (Prometheus Scrape + Grafana Dashboard-Platzhalter).
Repository-Layout
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
Kompromisse (die echten)
- Cookie-Sessions sind langweilig. Das ist ein Feature. Sie funktionieren hinter Proxys und halten die Authentifizierungslogik vom Frontend fern. Der Nachteil: Man muss SameSite/Secure/path richtig einstellen, sonst jagt man „zufällige“ Anmeldefehler.
- Eine /api-Oberfläche ist sauber, bedeutet aber auch, dass man streng sein muss, was öffentlich und was nur für Administratoren ist. Keine Admin-Endpunkte preisgeben, nur weil es bequem ist.
- Prisma kann im Sinne des Austauschs von Anbietern „multi-db-fähig“ sein. Es ist keine magische Brücke zwischen SQL und Mongo mit einem einzigen Schema. Wenn jemand das behauptet, hat er es noch nicht ausgeliefert.
- Next.js SSR ist gut für SEO und die erste Ladezeit. Es kann aber auch die API überlasten, wenn man Caching und Revalidierung nicht bewusst einstellt.
Routing-Vertrag (NGINX als Front-Door)
Wir leiten vorerst alles über eine Domain. Das ist einfacher zu debuggen. Wenn wir den Admin-Bereich später auf eine Subdomain verschieben, werden wir den Cookie-Geltungsbereich und die CSRF-Annahmen überprüfen.
# 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;
}
}
API-Paket (Fastify) – Sessions und Authentifizierung
Die API ist für Authentifizierung und Session-Status zuständig. Die Apps erstellen niemals Tokens. Sie senden lediglich Cookies zurück. Halten Sie die Session-Nutzlast klein. Benutzer-ID + Rolle. Das ist alles.
// 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);
}
};
}
DB-Paket (Prisma) – einzelner Client, gemeinsame Typen
Wir stellen einen PrismaClient von @platform/db zur Verfügung. Die API importiert diesen und nur diesen. Wenn Sie einen zweiten Prisma-Client in einer App erstellen, weil es „schneller zu prototypisieren“ ist, schaffen Sie nur einen zukünftigen Vorfall.
// packages/db/src/index.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
Admin-App – Cookie-Session und /me-Schutz
Admin ist eine normale Next.js App Router-Anwendung. Das Besondere ist, dass sie die API als Quelle der Wahrheit behandeln und beim Aufruf von /api/me immer Cookies mitsenden muss.
// 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>Unauthorized. Go to /admin/login.</p>
</body>
</html>
);
}
return (
<html>
<body>{children}</body>
</html>
);
}
Schritt für Schritt: Admin-Login-Flow
- Browser übermittelt Anmeldedaten: POST /api/login
- NGINX leitet /api/* an Fastify weiter
- Fastify validiert Benutzer gegen DB (Prisma)
- Fastify schreibt Session in fakeRedis-Speicher (Entwicklung) und gibt Set-Cookie: sid=… zurück
- Browser speichert das Cookie (HTTP-only)
- Admin-Serverkomponenten rufen GET /api/me mit angehängtem Cookie auf
- Admin rendert Dashboard nur, wenn /me ok=true zurückgibt
Eine Sache, die schiefgelaufen ist (damit wir sie nicht wiederholen)
Wir hatten eine Anmeldeschleife, obwohl /api/login 200 zurückgab. Das Cookie wurde nie an /api/me zurückgesendet. Die Ursache war langweilig: Cookie-Pfad + Proxy-Routing-Fehler. Wir haben das Cookie versehentlich für /admin gesetzt, aber /api/me befindet sich unter /api. Der Browser hat genau das getan, was er sollte. Er hat das Cookie nicht gesendet. Die Lösung war, den Cookie-Pfad auf „/“ zu setzen und zu bestätigen, dass NGINX die Pfade nicht so umschrieb, dass es zu Problemen kam.
Projektplan (zuerst liefern, später verfeinern)
- Phase 0: Monorepo-Verkabelung (Turbo, TS-Konfiguration, gemeinsame Paketimporte). Schlank halten.
- Phase 1: API-Authentifizierungs-/Session-Endpunkte: /login, /logout, /me. Grundlegende Ratenbegrenzung für /login hinzufügen.
- Phase 2: Admin MVP: Anmeldebildschirm + serverseitiger Schutz + minimale Dashboard-Shell.
- Phase 3: Content-Lese-Endpunkte für die öffentliche Plattform: /api/content/* (zuerst nur lesend).
- Phase 4: Content-CRUD im Admin + Medien-Upload (einfach beginnen, dann später den Speicher wechseln).
- Phase 5: Überwachung und Härtung: Prometheus-Metrik-Endpunkt, Grafana-Dashboards, Header, Backups.
Definition von „Fertig“ (damit wir später nicht streiten)
- Ein frischer Browser kann sich bei /admin anmelden und bleibt über Aktualisierungen hinweg angemeldet.
- Die öffentliche Plattform rendert SSR-Inhalte von /api/content, ohne nur für Administratoren zugängliche Endpunkte freizulegen.
- NGINX leitet /, /admin, /api in docker-compose korrekt weiter.
- Die API kann neu gestartet werden, ohne das Session-Verhalten zu beschädigen (Entwicklungs-Speicher-Resets sind in Ordnung, aber sie muss graceful fehlschlagen).
- Mindestens ein Prometheus-Scrape-Ziel existiert (auch wenn Grafana-Panels Platzhalter sind).
Wenn Sie einen neuen Endpunkt implementieren, entscheiden Sie zuerst: Ist er öffentlich, nur für Administratoren oder intern? Raten Sie nicht. Dann platzieren Sie ihn hinter dem richtigen Präfix und schützen ihn. Das ist das ganze Spiel.
Related Articles

Unternehmensfähige mandantenfähige Architektur für eine internationale Plattform
Loving Rocks ist eine Hochzeitsplattform auf Unternehmensniveau, konzipiert mit einer echten Mehrmandantenarchitektur, isolierten Datenbanken pro Mandant und integrierter Internationalisierung für globale Skalierbarkeit, Sicherheit und langfristige Betriebsstabilität.

Multi-Datenbank-Architektur mit Prisma 7: Ein Deep Dive für Experten
Die Verwaltung komplexer Datenlandschaften erfordert moderne Architekturen. Prisma 7 bietet erweiterte Funktionen für die Multi-Datenbank-Integration und adressiert die Herausforderungen der Polyglot Persistence.