Практическая архитектура монорепозитория с Next.js, Fastify, Prisma и NGINX

Иллюстрация
Монорепозиторий платформы: Next.js (Публичный + Админ) + Fastify API + Prisma DB + NGINX
Этот документ описывает текущую целевую архитектуру монорепозитория с публичным сайтом на Next.js, административным приложением на Next.js с сессиями на основе куки, Fastify API и пакетом базы данных на основе Prisma. Он написан для членов команды, которые будут работать с несколькими пакетами и нуждаются в предсказуемых границах.
Ограничения, которые мы не обсуждаем (пока)
- Публичный сайт должен быть SSR (Next.js App Router). Никаких сокращений только для SPA.
- Публичный сайт взаимодействует с бэкендом только через публичные маршруты API по адресу /api (например, /api/content, /api/media).
- Админ использует аутентификацию на основе куки-сессий. Куки только для HTTP. Никаких токенов в localStorage.
- API — Fastify. Сессии через @fastify/session. Хранилище «fakeRedis» для разработки; Redis в продакшене позже.
- Доступ к БД осуществляется только через @platform/db. Никаких прямых клиентов Prisma, разбросанных по приложениям.
- NGINX завершает TLS и маршрутизирует /, /admin и /api. Один домен подходит; поддомен необязателен позже.
- Мониторинг существует с первого дня (сбор данных Prometheus + заглушки для дашбордов Grafana).
Структура репозитория
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
Компромиссы (реальные)
- Куки-сессии скучны. Это особенность. Они работают за прокси и удерживают логику аутентификации вне фронтенда. Недостаток: вы должны правильно настроить SameSite/Secure/path, иначе будете гоняться за «случайными» ошибками входа.
- Единый интерфейс /api чист, но это также означает, что вы должны строго разделять публичные и только для администратора эндпоинты. Не допускайте утечки административных эндпоинтов просто потому, что это удобно.
- Prisma может быть «готовой к работе с несколькими БД» в смысле замены провайдеров. Это не волшебный мост между SQL и Mongo с единой схемой. Если кто-то говорит, что это так, они еще не выпустили продукт.
- Next.js SSR хорош для SEO и первой загрузки. Он также может перегрузить API, если вы намеренно не настроите кеширование и ревалидацию.
Контракт маршрутизации (NGINX как входная дверь)
Пока мы маршрутизируем все через один домен. Это проще для отладки. Если позже мы перенесем админку на поддомен, мы пересмотрим область действия куки и предположения о 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;
}
}
Пакет API (Fastify) — сессии и аутентификация
API владеет аутентификацией и состоянием сессии. Приложения никогда не выпускают токены. Они просто отправляют куки обратно. Сохраняйте полезную нагрузку сессии небольшой. ID пользователя + роль. Вот и все.
// 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);
}
};
}
Пакет БД (Prisma) — один клиент, общие типы
Мы предоставляем один PrismaClient из @platform/db. API импортирует только его. Если вы создадите второй клиент Prisma в приложении, потому что это «быстрее для прототипирования», вы просто создадите будущий инцидент.
// packages/db/src/index.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
Админ-приложение — куки-сессия и защита /me
Админ — это обычное приложение Next.js App Router. Единственная особенность заключается в том, что оно должно рассматривать API как источник истины и всегда включать куки при вызове /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>
);
}
Пошагово: процесс входа администратора
- Браузер отправляет учетные данные: POST /api/login
- NGINX перенаправляет /api/* на Fastify
- Fastify проверяет пользователя по БД (Prisma)
- Fastify записывает сессию в хранилище fakeRedis (разработка) и возвращает Set-Cookie: sid=…
- Браузер сохраняет куки (только HTTP)
- Серверные компоненты админки вызывают GET /api/me с прикрепленным куки
- Админка отображает дашборд только если /me возвращает ok=true
Одна вещь, которая пошла не так (чтобы мы не повторяли)
У нас был цикл входа, хотя /api/login возвращал 200. Куки никогда не отправлялись обратно на /api/me. Причина была скучной: несоответствие пути куки и маршрутизации прокси. Мы случайно установили куки для /admin, но /api/me находится по адресу /api. Браузер сделал именно то, что должен был. Он не отправил куки. Решение заключалось в установке пути куки на «/» и подтверждении того, что NGINX не переписывал пути таким образом, чтобы это нарушало работу.
План проекта (сначала выпуск, потом доработка)
- Фаза 0: настройка монорепозитория (Turbo, TS config, импорты общих пакетов). Сохраняйте его минималистичным.
- Фаза 1: Эндпоинты аутентификации/сессий API: /login, /logout, /me. Добавить базовое ограничение скорости на /login.
- Фаза 2: MVP админки: экран входа + серверная защита + минимальная оболочка дашборда.
- Фаза 3: Эндпоинты для чтения контента для публичной платформы: /api/content/* (сначала только чтение).
- Фаза 4: CRUD контента в админке + загрузка медиа (начните с простого, затем замените хранилище).
- Фаза 5: Мониторинг и усиление безопасности: эндпоинт метрик Prometheus, дашборды Grafana, заголовки, резервные копии.
Определение готовности (чтобы потом не спорить)
- Новый браузер может войти в /admin и оставаться авторизованным при обновлениях страницы.
- Публичная платформа отображает SSR контент из /api/content, не раскрывая эндпоинты только для администратора.
- NGINX корректно маршрутизирует /, /admin, /api в docker-compose.
- API может перезапускаться без нарушения поведения сессии (сбросы хранилища разработки допустимы, но он должен завершаться корректно).
- Существует хотя бы одна цель сбора метрик Prometheus (даже если панели Grafana являются заглушками).
Если вы реализуете новый эндпоинт, сначала решите: он публичный, только для администратора или внутренний. Не гадайте. Затем поместите его за правильный префикс и защитите. В этом вся суть.
Related Articles

Мультитенантная архитектура корпоративного уровня для международной платформы
Loving Rocks является корпоративной свадебной платформой, разработанной с истинной многоарендной архитектурой, изолированными базами данных для каждого арендатора и встроенной интернационализацией для глобальной масштабируемости, безопасности и долгосрочной операционной стабильности.

Мульти-базовая архитектура с Prisma 7: Глубокое погружение для экспертов
Управление сложными ландшафтами данных требует современных архитектур. Prisma 7 предлагает расширенные функции для интеграции с несколькими базами данных и решает проблемы полиглотной персистентности.