Praktična monorepo arhitektura sa Next.js, Fastify, Prisma i NGINX

Illustration
Platformski Monorepo: Next.js (Javni + Admin) + Fastify API + Prisma DB + NGINX
Ovaj dokument opisuje trenutnu ciljnu arhitekturu za monorepo sa javnim Next.js sajtom, Next.js admin aplikacijom sa sesijama zasnovanim na kolačićima, Fastify API-jem i DB paketom zasnovanim na Prismi. Napisan je za članove tima koji će raditi sa više paketa i potrebne su im predvidive granice.
Ograničenja o kojima ne pregovaramo (za sada)
- Javni sajt mora biti SSR (Next.js App Router). Bez prečica samo za SPA.
- Javni sajt komunicira sa bekendom samo putem javnih API ruta pod /api (npr. /api/content, /api/media).
- Admin koristi autentifikaciju sesije zasnovanu na kolačićima. HTTP-only kolačić. Bez tokena u localStorage-u.
- API je Fastify. Sesije putem @fastify/session. „fakeRedis“ skladište za razvoj; Redis u produkciji kasnije.
- Pristup bazi podataka je samo putem @platform/db. Bez direktnih Prisma klijenata razbacanih po aplikacijama.
- NGINX terminira TLS i rutira /, /admin i /api. Jedan domen je u redu; poddomen je opcionalan kasnije.
- Monitoring postoji od prvog dana (Prometheus scrape + Grafana dashboard placeholderi).
Raspored repozitorijuma
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
Kompromisi (pravi)
- Sesije sa kolačićima su dosadne. To je funkcija. Rade iza proksija i drže logiku autentifikacije van frontenda. Loša strana: morate ispravno podesiti SameSite/Secure/path, inače ćete juriti „nasumične“ greške pri prijavi.
- Jedna /api površina je čista, ali to takođe znači da morate biti strogi u pogledu toga šta je javno, a šta samo za administratore. Ne propuštajte administratorske krajnje tačke samo zato što je to zgodno.
- Prisma može biti „spremna za više baza podataka“ u smislu zamene provajdera. Nije magični most između SQL-a i Mongo-a sa jednom šemom. Ako neko kaže da jeste, nije je isporučio.
- Next.js SSR je dobar za SEO i prvo učitavanje. Takođe može preopteretiti API ako namerno ne podesite keširanje i revalidaciju.
Ugovor o rutiranju (NGINX kao ulazna vrata)
Za sada sve rutiramo kroz jedan domen. Jednostavnije je za debagovanje. Ako kasnije premestimo admin na poddomen, ponovo ćemo razmotriti opseg kolačića i pretpostavke o CSRF-u.
# 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) — sesije i autentifikacija
API poseduje autentifikaciju i stanje sesije. Aplikacije nikada ne kreiraju tokene. One samo šalju kolačiće nazad. Neka sadržaj sesije bude mali. ID korisnika + uloga. To je to.
// 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) — jedan klijent, deljeni tipovi
Izlažemo jedan PrismaClient iz @platform/db. API uvozi to i samo to. Ako kreirate drugi Prisma klijent u aplikaciji jer je „brže za prototipovanje“, samo stvarate budući incident.
// packages/db/src/index.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
Admin aplikacija — sesija sa kolačićima i /me zaštita
Admin je normalna Next.js App Router aplikacija. Jedini poseban deo je da mora tretirati API kao izvor istine i uvek uključivati kolačiće prilikom pozivanja /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>Unauthorized. Go to /admin/login.</p>
</body>
</html>
);
}
return (
<html>
<body>{children}</body>
</html>
);
}
Korak po korak: tok prijave administratora
- Pretraživač šalje akreditive: POST /api/login
- NGINX prosleđuje /api/* na Fastify
- Fastify validira korisnika u odnosu na DB (Prisma)
- Fastify upisuje sesiju u fakeRedis skladište (dev) i vraća Set-Cookie: sid=…
- Pretraživač čuva kolačić (HTTP-only)
- Admin serverske komponente pozivaju GET /api/me sa priloženim kolačićem
- Admin prikazuje kontrolnu tablu samo ako /me vrati ok=true
Jedna stvar koja je pošla naopako (da je ne ponavljamo)
Imali smo petlju za prijavu iako je /api/login vratio 200. Kolačić nikada nije poslat nazad na /api/me. Uzrok je bio dosadan: neusklađenost putanje kolačića + rutiranja proksija. Slučajno smo postavili kolačić za /admin, ali /api/me živi pod /api. Pretraživač je uradio tačno ono što je trebalo. Nije poslao kolačić. Rešenje je bilo da se putanja kolačića postavi na „/“ i potvrdi da NGINX nije prepisivao putanje na način koji bi to pokvario.
Plan projekta (prvo isporuka, kasnije dorada)
- Faza 0: povezivanje monorepoa (Turbo, TS konfiguracija, uvoz deljenih paketa). Neka bude jednostavno.
- Faza 1: API krajnje tačke za autentifikaciju/sesiju: /login, /logout, /me. Dodati osnovno ograničenje stope na /login.
- Faza 2: Admin MVP: ekran za prijavu + serverska zaštita + minimalna ljuska kontrolne table.
- Faza 3: Krajnje tačke za čitanje sadržaja za javnu platformu: /api/content/* (prvo samo za čitanje).
- Faza 4: CRUD sadržaja u adminu + otpremanje medija (početi jednostavno, zatim kasnije zameniti skladište).
- Faza 5: Nadzor i učvršćivanje: Prometheus krajnja tačka za metrike, Grafana kontrolne table, zaglavlja, rezervne kopije.
Definicija završenog (da se kasnije ne bismo svađali)
- Novi pretraživač se može prijaviti na /admin i ostati prijavljen tokom osvežavanja.
- Javna platforma prikazuje SSR sadržaj sa /api/content bez izlaganja krajnjih tačaka samo za administratore.
- NGINX ispravno rutira /, /admin, /api u docker-compose-u.
- API se može restartovati bez narušavanja ponašanja sesije (resetovanja dev skladišta su u redu, ali mora graciozno da otkaže).
- Postoji barem jedna Prometheus scrape meta (čak i ako su Grafana paneli placeholderi).
Ako implementirate novu krajnju tačku, prvo odlučite: da li je javna, samo za administratore ili interna. Ne nagađajte. Zatim je stavite iza odgovarajućeg prefiksa i zaštitite je. To je cela igra.