
Шпаргалка по основным моментам «next.js»
Шпаргалка команд
# Проект
npm install
npm run dev
npm run build && npm run start
npm run lint
npm run test
# Prisma
npm run db:migrate # dev: новая миграция + apply
npm run db:migrate:deploy # prod/CI: только apply
npm run db:push # прототип без файла миграции
npm run db:generate # обновить клиент
npm run db:seed # тестовые данные
npm run db:studio # GUI
Вводная часть
Next.js — React-фреймворк полного цикла для создания веб-приложений. Он объединяет фронтенд (UI), серверную логику, API и оптимизацию сборки в одном проекте.
Зачем Next.js, а не «голый» React
| Подход | Плюсы | Минусы |
|---|---|---|
| Create React App / Vite + React | Простой старт, полный контроль | Нужно отдельно настраивать SSR, роутинг, API, SEO |
| Next.js | Роутинг, SSR/SSG, API Routes, оптимизация изображений из коробки | Свои соглашения (App Router), привязка к экосистеме Vercel |
App Router (маршрутизация на основе файлов)
С версии 13+ основной способ организации кода — App Router. Каждая папка в src/app/ соответствует URL-сегменту.
src/app/
├── layout.tsx # корневой layout (HTML, шрифты, глобальные стили)
├── (public)/ # route group — не влияет на URL
│ ├── layout.tsx
│ ├── page.tsx # GET /
│ └── watch/[slug]/
│ └── page.tsx # GET /watch/:slug
├── (admin)/
│ └── admin/
│ ├── layout.tsx
│ └── videos/
│ └── page.tsx # GET /admin/videos
└── api/
└── videos/
└── route.ts # GET/POST /api/videos
Route groups (public) и (admin) позволяют разделить layout'ы (публичная зона vs админка) без изменения пути в URL.
Server Components и Client Components
По умолчанию все компоненты в App Router — Server Components. Они выполняются только на сервере: можно напрямую обращаться к БД, читать файлы, использовать секреты из .env.
// src/app/(public)/page.tsx — Server Component
import { listPublishedVideos } from "@/lib/queries/videos";
export default async function HomePage() {
const videos = await listPublishedVideos(); // запрос к Prisma на сервере
return (/* ... */);
}
Если нужны хуки React (useState, useEffect), обработчики событий (onClick) или браузерные API — добавьте директиву "use client" в начало файла:
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Правило: данные загружайте на сервере, интерактивность — в небольших Client Components.
API Routes (Route Handlers)
Файлы route.ts в src/app/api/ — это HTTP-эндпоинты. Экспортируются функции по имени метода: GET, POST, PUT, PATCH, DELETE.
// src/app/api/videos/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET() {
const videos = await listPublishedVideos();
return NextResponse.json({ videos });
}
export async function POST(request: NextRequest) {
const body = await request.json();
// валидация, запись в БД...
return NextResponse.json({ ok: true }, { status: 201 });
}
Динамические маршруты и параметры
Сегменты в квадратных скобках — динамические:
[slug]— один параметр (/watch/my-video)[id]— в API (/api/videos/abc-123)
В Server Components параметры приходят через props:
export default async function WatchPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}
Рендеринг: SSR, SSG, ISR
| Режим | Когда обновляется HTML | Пример использования |
|-||-|
| Динамический (SSR) | При каждом запросе | Лента с новыми видео, админка |
| Статический (SSG) | При сборке (next build) | Страницы без персонализации |
| ISR | По таймеру или on-demand | Блог с редкими обновлениями |
Middleware
Файл src/middleware.ts выполняется до обработки запроса. Подходит для редиректов, проверки сессии, rate limiting.
Инициализация проекта
Создание нового проекта
npx create-next-app@latest my-app
Рекомендуемые опции:
| Вопрос | Ответ |
|---|---|
| TypeScript | Yes |
| ESLint | Yes |
| Tailwind CSS | Yes |
src/ directory | Yes |
| App Router | Yes |
| Turbopack | по желанию (в dev можно включить) |
Структура после инициализации
my-app/
├── prisma/
│ ├── schema.prisma
│ ├── migrations/
│ └── seed.ts
├── src/
│ ├── app/ # страницы и API
│ ├── components/ # UI-компоненты
│ └── lib/ # утилиты, Prisma-клиент, запросы к БД
├── public/ # статика (favicon, robots.txt)
├── .env # локальные секреты (не коммитить!)
├── .env.example # шаблон переменных для команды
├── next.config.ts
├── package.json
└── docker-compose.yml # PostgreSQL и прочие сервисы
Установка Prisma
npm install @prisma/client
npm install -D prisma
npx prisma init
В prisma/schema.prisma укажите PostgreSQL и источник URL из окружения:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Скрипты npm
Типичный набор в package.json:
{
"scripts": {
"dev": "next dev",
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
"test": "vitest run",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:push": "prisma db push",
"db:seed": "prisma db seed",
"db:studio": "prisma studio"
}
}
Первый запуск приложения
npm install
npm run db:migrate # создать таблицы
npm run db:seed # опционально: тестовые данные
npm run dev # http://localhost:3000
Singleton Prisma Client
В dev-режиме Next.js перезагружает модули при hot reload. Без паттерна singleton создаётся слишком много подключений к БД. Используйте один экземпляр:
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
Запросы к БД выносите в src/lib/queries/ — страницы и API остаются тонкими.
Проведение миграций в Prisma
Миграция — версионируемое изменение схемы БД (новая таблица, поле, индекс). Prisma хранит историю в prisma/migrations/.
Основные команды
| Команда | Назначение | Когда использовать |
|---|---|---|
npx prisma migrate dev | Создать и применить миграцию в dev | Локальная разработка |
npx prisma migrate deploy | Применить существующие миграции | CI/CD, production |
npx prisma db push | Синхронизировать схему без файла миграции | Быстрые прототипы, throwaway БД |
npx prisma generate | Сгенерировать TypeScript-клиент | После каждого изменения schema.prisma |
npx prisma studio | GUI для просмотра данных | Отладка |
npx prisma db seed | Заполнить БД начальными данными | После миграций |
В проектах linuxSystems те же команды доступны через npm: npm run db:migrate, npm run db:migrate:deploy и т.д.
Типичный цикл разработки
1. Измените prisma/schema.prisma
model Video {
id String @id @default(uuid())
title String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
2. Создайте и примените миграцию
npm run db:migrate
# Prisma спросит имя миграции, например: add_video_table
Prisma:
- сравнит схему с текущей БД;
- создаст папку
prisma/migrations/YYYYMMDDHHMMSS_add_video_table/migration.sql; - выполнит SQL против
DATABASE_URL; - запустит
prisma generate.
3. Используйте клиент в коде
import { prisma } from "@/lib/prisma";
const video = await prisma.video.create({
data: { title: "Demo", slug: "demo" },
});
Типы полей (Video, VideoStatus) появятся автоматически из сгенерированного клиента.
Первая миграция (init)
При инициализации проекта создаётся миграция вроде 20260609001146_init с полным SQL создания таблиц. Её нужно закоммитить в git вместе с schema.prisma — так вся команда и CI получают одинаковую схему.
Production и CI
На сервере не используйте migrate dev (она может запросить интерактивное имя и создавать новые миграции). Только:
npx prisma migrate deploy
npx prisma generate
npm run build
Скрипт build в наших проектах уже включает prisma generate перед next build.
Seed (начальные данные)
В package.json:
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
Файл prisma/seed.ts создаёт демо-записи через PrismaClient. Запуск:
npm run db:seed
Seed идемпотентен там, где это возможно (проверка upsert / существующих slug), чтобы повторный запуск не ломал данные.
db push vs migrate dev
db push | migrate dev | |
|---|---|---|
| Файл миграции | Не создаёт | Создаёт |
| История в git | Нет | Да |
| Production | Не подходит | Да (migrate deploy) |
| Скорость | Быстро | Чуть дольше |
Архитектура и соглашения
Слои приложения
┌─────────────────────────────────────────┐
│ src/app/ страницы, layouts │
│ src/components/ переиспользуемый UI │
├─────────────────────────────────────────┤
│ src/lib/queries/ доступ к данным │
│ src/lib/ prisma, s3, валидация│
├─────────────────────────────────────────┤
│ Prisma --> PostgreSQL │
│ S3/MinIO, Redis, Worker (вне Next.js) │
└─────────────────────────────────────────┘
- Страницы — композиция компонентов и вызов query-функций.
lib/queries/— единственное место для Prisma-запросов сложной логики.lib/validations/— Zod-схемы для API и форм.- Воркеры (
worker/) — долгие задачи (транскодинг) вне HTTP-цикла Next.js.
Валидация на границе
Внешние данные (тело запроса, query-параметры) всегда проверяйте до записи в БД:
const parsed = createVideoSchema.safeParse(body);
if (!parsed.success) {
return validationError(parsed.error);
}
Алиасы импортов
В tsconfig.json настроен путь @/* → src/*:
import { prisma } from "@/lib/prisma";
import { VideoCard } from "@/components/video/video-card";
Форма: запись данных в БД
Пример ниже создаёт страницу /videos/new с формой «название + slug» и сохраняет запись в таблицу Video (модель из раздела про миграции). Используется Server Action — рекомендуемый способ для простых форм в админке и внутренних страницах (catshredias-blog: категории, теги, проекты).
Схема потока данных
Browser (форма)
↓ FormData
Server Action (actions.ts)
↓ Zod safeParse
prisma.video.create()
↓
PostgreSQL
Данные не пишутся в БД из Client Component — только через серверный код ("use server" или route.ts).
Структура файлов
src/
├── app/
│ └── videos/
│ ├── actions.ts # Server Action
│ └── new/
│ └── page.tsx # страница /videos/new
├── components/
│ └── create-video-form.tsx # Client Component с <form>
└── lib/
├── prisma.ts
└── validations/
└── video.ts # Zod-схема
Компоненты UI лежат в src/components/, а не внутри src/app/. Алиас @/* указывает на src/*, поэтому импорт выглядит так: @/components/create-video-form, не @/prisma/components/....
1. Zod-схема — src/lib/validations/video.ts
import { z } from "zod";
export const createVideoSchema = z.object({
title: z.string().trim().min(1, "Введите название").max(200),
slug: z
.string()
.trim()
.min(1, "Введите slug")
.max(200)
.regex(/^[a-z0-9-]+$/, "Только латиница, цифры и дефис"),
});
export type VideoFormState = {
error?: string;
fieldErrors?: Record<string, string[]>;
};
2. Server Action — src/app/videos/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { createVideoSchema, type VideoFormState } from "@/lib/validations/video";
export async function createVideoAction(
_prev: VideoFormState,
formData: FormData,
): Promise<VideoFormState> {
const parsed = createVideoSchema.safeParse({
title: formData.get("title"),
slug: formData.get("slug"),
});
if (!parsed.success) {
return { fieldErrors: parsed.error.flatten().fieldErrors };
}
try {
await prisma.video.create({ data: parsed.data });
} catch {
return { error: "Видео с таким slug уже существует" };
}
revalidatePath("/videos");
redirect("/videos");
}
safeParse— проверка без исключений; ошибки полей возвращаются в форму.revalidatePath— обновляет кэш списка после записи.redirect— переход на страницу списка (опционально).
3. Форма — src/components/create-video-form.tsx
"use client";
import { useActionState } from "react";
import { createVideoAction } from "@/app/videos/actions";
import type { VideoFormState } from "@/lib/validations/video";
const initialState: VideoFormState = {};
export function CreateVideoForm() {
const [state, formAction, pending] = useActionState(createVideoAction, initialState);
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="title" className="mb-1 block text-sm font-medium">
Название
</label>
<input
id="title"
name="title"
required
className="min-h-11 w-full rounded-lg border px-3"
/>
{state.fieldErrors?.title ? (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.title[0]}</p>
) : null}
</div>
<div>
<label htmlFor="slug" className="mb-1 block text-sm font-medium">
Slug
</label>
<input
id="slug"
name="slug"
required
placeholder="my-video"
className="min-h-11 w-full rounded-lg border px-3"
/>
{state.fieldErrors?.slug ? (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.slug[0]}</p>
) : null}
</div>
<button type="submit" disabled={pending} className="rounded-lg bg-black px-4 py-2 text-white">
{pending ? "Сохранение…" : "Создать"}
</button>
{state.error ? (
<p className="text-sm text-red-600" role="alert">
{state.error}
</p>
) : null}
</form>
);
}
useActionState связывает форму с Server Action: pending блокирует кнопку, state показывает ошибки после отправки.
4. Страница — src/app/videos/new/page.tsx
import { CreateVideoForm } from "@/components/create-video-form";
export default function NewVideoPage() {
return (
<main className="mx-auto max-w-lg p-8">
<h1 className="text-2xl font-bold">Новое видео</h1>
<div className="mt-6">
<CreateVideoForm />
</div>
</main>
);
}
Альтернатива: API Route + fetch
Для публичных форм (контакты, комментарии) удобнее отдельный эндпоинт и fetch с JSON — так сделано в catshredias-blog (/api/contacts + contact-form.tsx). Схема та же: Zod на сервере обязателен; на клиенте можно добавить react-hook-form + zodResolver для мгновенной валидации полей.
| Сценарий | Подход |
|---|---|
| Админка, простая форма | Server Action + useActionState |
| Публичная форма, rate limit, JSON | src/app/api/.../route.ts + fetch |
Чеклист
- Модель в
schema.prisma+npm run db:migrate src/lib/prisma.ts(singleton)- Zod-схема в
src/lib/validations/ - Server Action или
route.tsсPOST - Client Component с
<form action={...}> - Server Component-страница, которая рендерит форму
- Проверка:
npm run db:studio— запись появилась в таблице
Стилизация
Tailwind CSS 4 подключается через PostCSS. Утилитарные классы в JSX, глобальные токены — в src/app/globals.css. Дизайн-токены (text-foreground, border-border) задают единый вид публичной зоны и админки.
Разработка и отладка
Локальный dev-сервер
npm run dev
По умолчанию приложение на http://localhost:3000. Hot Module Replacement обновляет Client Components без полной перезагрузки; Server Components пересчитываются при изменении server-кода.
Линтинг
npm run lint
Используется eslint-config-next — правила заточены под App Router и React 19.
Тесты
npm run test # unit-тесты (Vitest)
npm run test:integration # интеграционные (video-platform)
Интеграционные тесты могут требовать поднятой БД и переменных из .env.
Prisma Studio
Визуальный редактор данных:
npm run db:studio
Откроется веб-интерфейс на http://localhost:5555 — удобно проверить результат seed и миграций.
Сборка production локально
npm run build
npm run start
next build проверяет типы, собирает страницы и API. Ошибки Prisma или отсутствующие env-переменные проявятся на этом этапе.
Заключение
Next.js в экосистеме linuxSystems — это серверный React с чёткой файловой структурой: страницы в src/app/, данные через Prisma, API в route.ts, интерактивность в Client Components. Prisma миграции дают воспроизводимую схему PostgreSQL от локальной машины до production.
Минимальный путь от нуля до работающего приложения:
create-next-app+ Prisma +.envdocker compose up -d- Описать модели в
schema.prisma npm run db:migrate- Singleton
prismaвsrc/lib/prisma.ts - Query-функции → страницы и API
npm run dev