Перейти к содержимому
Catshredia
← К блогу

Шпаргалка по основным моментам «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

Рекомендуемые опции:

ВопросОтвет
TypeScriptYes
ESLintYes
Tailwind CSSYes
src/ directoryYes
App RouterYes
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 studioGUI для просмотра данныхОтладка
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 pushmigrate 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, JSONsrc/app/api/.../route.ts + fetch

Чеклист

  1. Модель в schema.prisma + npm run db:migrate
  2. src/lib/prisma.ts (singleton)
  3. Zod-схема в src/lib/validations/
  4. Server Action или route.ts с POST
  5. Client Component с <form action={...}>
  6. Server Component-страница, которая рендерит форму
  7. Проверка: 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.

Минимальный путь от нуля до работающего приложения:

  1. create-next-app + Prisma + .env
  2. docker compose up -d
  3. Описать модели в schema.prisma
  4. npm run db:migrate
  5. Singleton prisma в src/lib/prisma.ts
  6. Query-функции → страницы и API
  7. npm run dev

Комментарии

Пока нет комментариев.

Загрузка…