Next.js может быть и интерфейсом, и серверным слоем приложения. На практике важно не просто отрендерить страницу, а разграничить данные, права, кеш и внешний backend.
Соберём типовой production-сценарий с каталогом и API. Он подходит и для самостоятельного Next.js-приложения, и для связки Next.js с NestJS.
Структура App Router
app/
(shop)/
products/
page.tsx
[id]/
page.tsx
(auth)/
login/
page.tsx
api/
products/
route.ts
[id]/
route.ts
layout.tsx
error.tsx
loading.tsx
Скобки в (shop) и (auth) группируют маршруты, не меняя URL. page.tsx отвечает за страницу, route.ts за HTTP endpoint, loading.tsx за состояние загрузки, error.tsx за ошибку конкретной части дерева.
Серверные компоненты остаются серверными по умолчанию. Директиву "use client" добавляют только компонентам, которым нужны события, состояние или браузерные API.
Серверный компонент с данными
import { ProductCard } from "./product-card";
export default async function ProductsPage() {
const response = await fetch(`${process.env.API_URL}/products`, {
next: { revalidate: 60, tags: ["products"] },
});
if (!response.ok) {
throw new Error("Не удалось загрузить каталог");
}
const products = await response.json();
return (
<main>
<h1>Каталог</h1>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</main>
);
}
Запрос выполняется на сервере. API_URL и учётные данные backend не попадают в клиентский бандл. revalidate: 60 разрешает кешировать ответ на минуту, а тег products позволяет сбросить его после изменения.
Если страница зависит от текущего пользователя, не кешируйте её публично. Используйте динамический рендеринг или раздельное кеширование данных, не связанных с пользователем.
Route handler для API
app/api/products/route.ts превращается в HTTP endpoint. Валидация и авторизация выполняются до записи:
import { z } from "zod";
import { requireUser } from "@/lib/auth";
import { createProduct } from "@/lib/products";
const createProductSchema = z.object({
name: z.string().trim().min(1).max(300),
price: z.number().positive(),
});
export async function POST(request: Request) {
const user = await requireUser();
if (!user) {
return Response.json(
{ error: { code: "UNAUTHORIZED" } },
{ status: 401 },
);
}
if (user.role !== "admin") {
return Response.json(
{ error: { code: "FORBIDDEN" } },
{ status: 403 },
);
}
const body = await request.json().catch(() => null);
const parsed = createProductSchema.safeParse(body);
if (!parsed.success) {
return Response.json(
{ error: { code: "VALIDATION_ERROR" } },
{ status: 400 },
);
}
const product = await createProduct(parsed.data);
return Response.json(product, { status: 201 });
}
Если JSON повреждён, request.json() выбросит ошибку. Здесь она превращается в null, после чего safeParse возвращает обычный 400, а не непредвиденный 500.
requireUser берёт сессию из cookie через выбранную auth-библиотеку. Не передавайте роль из формы и не доверяйте userId в теле запроса: сервер сам определяет пользователя и его права.
Для GET /api/products/[id] проверяют параметр и возвращают 404, если объект не найден. Для PATCH проверяют только разрешённые поля, а для DELETE - идемпотентность повторного удаления.
Server Actions
Server Action позволяет вызвать серверную операцию из формы без отдельного fetch на клиенте:
<form action={createProductAction}>
<input name="name" required />
<input name="price" type="number" min="0.01" step="0.01" required />
<button type="submit">Добавить</button>
</form>
"use server";
import { revalidateTag } from "next/cache";
import { requireUser } from "@/lib/auth";
import { createProduct } from "@/lib/products";
export async function createProductAction(formData: FormData) {
const user = await requireUser();
if (!user || user.role !== "admin") {
throw new Error("Недостаточно прав");
}
const parsed = createProductSchema.parse({
name: formData.get("name"),
price: Number(formData.get("price")),
});
await createProduct(parsed);
revalidateTag("products", "max");
}
Server Action является публичной серверной точкой входа, а не безопасным внутренним методом. Авторизация и валидация нужны так же, как в route handler.
Если действие может выполняться повторно, добавляйте бизнес-ключ идемпотентности. Не полагайтесь на двойной клик или повторную отправку формы.
Кеширование
У Next.js несколько уровней кеша, поэтому сначала решите, что именно кешируется:
- ответ
fetchс тегами иrevalidate; - результат серверного рендера страницы;
- клиентские данные в React Query или другом клиентском кеше;
- ответ внешнего NestJS API.
После создания или изменения объекта сбрасывайте только связанный тег через revalidateTag. Не очищайте весь сайт из-за одной операции.
Для персональных данных не используйте общий публичный кеш. Для публичного каталога кеш уместен, для корзины и прав пользователя - нет.
Next.js и NestJS
Разделение ролей упрощает систему:
- Next.js отвечает за страницы, формы и BFF-слой для интерфейса;
- NestJS владеет доменными операциями, транзакциями и интеграциями;
- браузер обращается к Next.js или к публичному API согласно выбранной архитектуре;
- серверный код Next.js обращается к NestJS через внутреннюю сеть.
Не дублируйте бизнес-правила в Server Actions и NestJS. Если операция меняет деньги или запасы, её владельцем должен быть один backend-контракт.
Секреты хранят только на сервере. Переменная NEXT_PUBLIC_* попадает в клиентскую сборку и поэтому не подходит для токенов и паролей.
Ошибки и наблюдаемость
В route handlers возвращайте стабильные коды ошибок и HTTP-статусы. В лог записывайте requestId, пользователя, маршрут и исход операции. Не логируйте session cookie и содержимое токенов.
Для страниц используйте error.tsx, чтобы показать понятное состояние вместо пустого экрана. Для healthcheck выделяйте отдельный endpoint, который не требует авторизации.
Тестирование
Проверяйте три уровня:
- Контракт route handler: 400 на невалидный вход, 401 без сессии, 403 без роли, 201 при успехе.
- Server Action: повторная отправка, запрет на запись, сброс нужного тега.
- E2E: пользователь проходит форму в браузере и видит результат.
Для контрактного теста мокают только внешние зависимости: auth и backend. Для интеграционного теста поднимают реальный Next.js и backend с тестовой базой.
Деплой
Проверьте:
- переменные окружения отделены от клиентских
NEXT_PUBLIC_*; - Node.js-версия соответствует требованиям выбранной Next.js;
- миграции выполняются отдельно от старта реплик;
next buildпроходит в CI;- тесты запускаются на итоговом образе;
- старые инстансы не получают трафик до готовности;
- SIGTERM не обрывает активные Server Actions и воркеры.
Serverless-платформа меняет ограничения по времени выполнения и фоновым задачам. Долгие очереди выносят в отдельный worker, а не удерживают внутри HTTP-запроса.
Коротко
- App Router объединяет серверные страницы и route handlers.
- Server Actions требуют той же авторизации и валидации, что и обычный API.
- Кеш разделяют по данным и пользователям, а не сбрасывают целиком.
- Next.js отвечает за интерфейс и BFF, NestJS - за доменные операции.
- Production-проверка включает безопасность, наблюдаемость, тесты и корректное завершение.
Что почитать дальше
- Next.js и серверный рендер — SSR, RSC и Server Actions по устройству.
- Браузер и сеть — cookie, XSS, CSRF и CSP.
- Контроллеры NestJS — backend-контракт для Next.js.