Next.js — фреймворк на основе React

Next.js — самый популярный React-фреймворк: роутинг, серверный рендеринг, оптимизация изображений и API-маршруты работают «из коробки». В этом уроке ты создашь приложение на App Router, разберёшься с серверными и клиентскими компонентами и задеплоишь проект на Vercel.

Уровень: средний 18 разделов 3–4 часа

1. Что такое Next.js и зачем он нужен

Next.js — это фреймворк поверх React. Он добавляет то, чего в «голом» React нет: многостраничный роутинг, серверный рендеринг, генерацию статики, API-маршруты и оптимизацию ресурсов. Проще говоря, React рисует интерфейс, а Next.js делает из этого готовый к деплою веб-сайт.

# Создание проекта
npx create-next-app@latest my-app

# Запуск в режиме разработки
cd my-app
npm run dev
# страница откроется на http://localhost:3000
При создании проекта create-next-app спросит про TypeScript, ESLint, Tailwind и App Router. Для учебного проекта можно отвечать «да» — все они подключаются без лишней настройки.

2. App Router и файловая структура

Современный Next.js использует App Router: каждая папка в app/ становится адресом, а файл page.tsx внутри неё — содержимым страницы. Роутинг получается файловым, без отдельного конфига.

my-app/
├── app/
│   ├── layout.tsx        // общий каркас всего приложения
│   ├── page.tsx          // страница "/"
│   ├── globals.css       // глобальные стили
│   ├── blog/
│   │   ├── page.tsx      // страница "/blog"
│   │   └── [slug]/
│   │       └── page.tsx  // страница "/blog/привет"
│   └── api/
│       └── route.ts      // API-маршрут "/api"
└── public/               // статические файлы
Файл layout.tsx оборачивает страницы: всё, что лежит внутри его JSX, остаётся общим при переходах — шапка, подвал, навигация. Писать их с нуля для каждой страницы не нужно.

3. Страницы, layout и метаданные

Самая простая страница — это компонент по умолчанию в app/page.tsx. Метаданные для SEO задаются экспортом metadata.

// app/page.tsx
export const metadata = {
  title: 'Главная | Мой сайт',
  description: 'Учебный сайт на Next.js'
}

export default function Home() {
  return <h1>Привет, Next.js!</h1>
}
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ru">
      <body>
        <header>Шапка сайта</header>
        {children}
        <footer>Подвал</footer>
      </body>
    </html>
  )
}
В App Router корневой layout обязателен и должен содержать теги <html> и <body>. Вложенные layout-ы оборачивают только свою часть приложения.

4. Роутинг и динамические пути

Динамический сегмент обозначается квадратными скобками в имени папки, например [slug]. Значение из адреса попадает в пропсы страницы через params.

// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
  const { slug } = await params
  return (
    <article>
      <h1>Пост: {slug}</h1>
    </article>
  )
}
ФайлАдрес
app/page.tsx/
app/about/page.tsx/about
app/blog/[slug]/page.tsx/blog/любое-значение
app/blog/[...slug]/page.tsx/blog/a/b/c (на любой вложенности)
app/blog/[[slug]]/page.tsxи /blog, и /blog/значение
Ссылки между страницами делай через компонент <Link href="/blog"> из пакета next/link. Он даёт быстрые переходы без полной перезагрузки страницы, в отличие от обычного <a>.

5. Серверные и клиентские компоненты

По умолчанию все компоненты в App Router — серверные: они выполняются на сервере и присылают в браузер уже готовый HTML. Клиентские компоненты помечаются директивой 'use client' и нужны для интерактива: обработчиков кликов, хуков useState и эффектов.

// app/Counter.tsx — клиентский компонент
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(count + 1)}>
      Кликов: {count}
    </button>
  )
}
// app/page.tsx — серверный компонент использует клиентский
import Counter from './Counter'

export default function Home() {
  return (
    <div>
      <h1>Счётчик</h1>
      <Counter />
    </div>
  )
}
Серверные компоненты не тянут за собой JavaScript в браузер — страница грузится быстрее. Используй 'use client' только там, где реально нужен интерактив.

6. Работа с данными: SSR, SSG и ISR

Next.js предлагает три режима отдачи страниц. По умолчанию страница рендерится на сервере при каждом запросе (SSR). generateStaticParams собирает статические страницы заранее (SSG), а revalidate обновляет кэш по расписанию (ISR).

// app/products/page.tsx — серверная загрузка данных
async function getProducts() {
  const res = await fetch('https://api.example.com/products')
  if (!res.ok) throw new Error('Ошибка загрузки')
  return res.json()
}

export const revalidate = 3600 // ISR: пересобирать раз в час

export default async function Products() {
  const products = await getProducts()
  return (
    <ul>
      {products.map((p) => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}
// app/blog/[slug]/page.tsx — статические пути
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

export default async function Post({ params }) {
  const { slug } = await params
  const post = await getPost(slug)
  return <article>{post.title}</article>
}
РежимКогдаПрименение
SSRна каждый запросличный кабинет, актуальные данные
SSGодин раз на сборкеблог, документация, витрины
ISRпо расписаниюкаталог, новости, цены
Начни с SSG для статики и SSR для персонализированных страниц. ISR подключай, когда статика начала «устаревать», — это золотая середина.

7. API-маршруты и Server Actions

Файл app/api/*/route.ts превращается в API-эндпоинт: экспорты GET и POST обрабатывают соответствующие запросы. Такой маршрут удобен для форм и публичных endpoint-ов.

// app/api/feedback/route.ts
export async function POST(request) {
  const body = await request.json()
  // сохранить body.name в базу...
  return Response.json({ ok: true }, { status: 201 })
}
// Приём обращений с клиента
fetch('/api/feedback', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Анна', text: 'Отличный курс!' })
})
Не клади в API-маршруты секреты, если маршрут публичный. Работа с базой, ключами и токенами должна оставаться в серверном коде — например, в Server Actions или серверных компонентах.

8. Стили и оптимизация изображений

Использовать можно любой подход: глобальный CSS, CSS Modules, Tailwind CSS или CSS-in-JS. Для картинок в Next.js есть компонент next/image, который сжимает, кэширует и подгружает изображения с задержкой.

// app/page.tsx — оптимизированное изображение
import Image from 'next/image'

export default function Home() {
  return (
    <Image
      src="/hero.jpg"
      alt="Заголовок страницы"
      width={1200}
      height={600}
      priority
    />
  )
}
// Стили, ограниченные компонентом (CSS Modules)
Карточка
Для next/image всегда указывай width и height или используй fill, чтобы браузер не «прыгал» при загрузке страницы. Домашний баннер помечай флагом priority — так он загрузится быстрее.

9. Сборка и проверка перед деплоем

Перед отправкой на сервер проект необходимо собрать продакшен-версию. Команда npm run build покажет список страниц и способы их генерации, а npm run start запустит собранное приложение локально.

npm run build
# ┌ /blog                 ○ SSG (static)
# ├ /blog/[slug]          ● SSG (static)
# └ /account              λ Server (dynamic)

npm run start
Символы в выводе сборки: ○ — статика, ● — статика с ISR, λ — серверная страница. Полезно проверять, что тяжёлые страницы не стали динамическими случайно.

10. Деплой на Vercel

Vercel — платформа от авторов Next.js: репозиторий, пуши и автодеплой предпросмотра на каждый pull request. Достаточно подключить GitHub-репозиторий, и Vercel сам соберёт и опубликует проект по HTTPS-ссылке.

# Автодеплой при пуше в main
git init
git add .
git commit -m "Первая версия блога"
git remote add origin https://github.com/user/my-app.git
git push -u origin main
// Next.js сам определяет команду сборки и настройки
// Vercel: Add New Project → Import → Deploy
На локальном уровне фокус на деталях: настроить свой домен, переменные окружения для секретов и ветки предпросмотра. Каждому PR создаётся отдельная ссылка — идеально для проверки командой.
Переменные окружения (ключи API, адреса баз данных) задавай в панели Vercel, а не в репозитории. Никогда не коммить файлы с ключами — это утечка секретов.

11. Middleware и защита маршрутов

Middleware выполняется на edge-уровне до рендеринга страницы и позволяет перенаправлять пользователей. Прослойка файла middleware.ts в корне проекта перехватывает запросы глобально.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('session');
  const path = req.nextUrl.pathname;

  if (path.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*']
};
Проверку обязательной авторизации держите и на сервере: в Server Component или getServerSideProps. Middleware — удобный первый щит, но не единственный.
Middleware нельзя использовать с помощью Node.js API (доступ к базе через драйвер). Лёгкие проверки — cookies, headers, переадресация — из edge-рантайма выполняются быстро.

12. Кэширование: fetch и revalidate

В Next.js по умолчанию данные fetch в Server Components кэшируются. Управление свежестью — в опциях запроса и настройках страницы.

// Кэш на 60 секунд
const res = await fetch('https://api.example.com/items', {
  next: { revalidate: 60 }
});

// Без кэша
const live = await fetch('https://api.example.com/live', {
  cache: 'no-store'
});
Не комбинируйте no-store с мутациями на той же странице: часть клиентов будет рендерить устаревший контент, часть — актуальный. Выберите единую стратегию для маршрута.

13. Метаданные и Open Graph

Полноценные метатеги улучшают SEO и красивую превью-карточку в мессенджерах. В Next.js метаданные задаются экспортом из Server Component или файлом generateMetadata.

export const metadata = {
  title: 'Магазин — доставка за один день',
  description: 'Онлайн-каталог с быстрой доставкой',
  openGraph: {
    title: 'Магазин',
    description: 'Доставка за один день',
    images: ['/og.jpg'],
    type: 'website'
  },
  twitter: { card: 'summary_large_image' }
};
Генерируйте metadata из данных: title = имя товара, description = первые 150 символов описания. Динамические страницы без этого показывают одинаковый title у всех товаров — низкий CTR.

Проверяйте превью через валидатор отладчика соцсети и не забывайте про sitemap.ts и robots.ts — их поддерживают локальные файлы в папке app/.

14. Мониторинг и наблюдаемость

Для продакшена важно видеть: скорость страниц, ошибки фронта и серверные исключения. Начните с карт витальности и сбора ошибок через логирование.

ИнструментНазначение
Vercel AnalyticsCore Web Vitals, страницы
Sentryошибки браузера и сервера
OpenTelemetryтрейсы и метрики
Логи в stdoutошибки API и рендера
// server.ts или route handler
try {
  const data = await loadData();
  return new Response(JSON.stringify(data));
} catch (err) {
  console.error('[API loadData]', err);
  return new Response('Ошибка', { status: 500 });
}
Настраивайте алерт на рост 5xx и времени ответа. Устанавливайте NEXT_PUBLIC_-переменные только для данных, которые безопасно показывать клиенту: они попадают в бандл.

15. Параллельные и перехватывающие маршруты

App Router поддерживает продвинутые паттерны композиции страниц. Параллельные маршруты (parallel routes) показывают на одной странице несколько независимых слотов — например, таблицу и детали клика — названных папками с префиксом @.

app/
  dashboard/
    @stats/page.jsx
    @orders/page.jsx
    layout.jsx

Слоты рендерятся одновременно и независимо, поэтому медленный график не блокирует остальную часть страницы. Перехватывающие маршруты с именем (.), (..) и (...) позволяют открыть модальное окно с содержимым другого маршрута, сохранив адресную строку прежней — классический UX карточки товара поверх каталога.

Обязательно задавайте файл default.jsx для каждого слота: при несовпадении маршрута вложенный контент использует это запасное состояние вместо пустой ошибки.
Сочетание параллельных слотов и loading.jsx даёт плавный поток скелетонов без «прыжков» макета.

16. Тестирование Next.js

Проекты на Next.js покрывают тестами на двух уровнях: unit/интеграционные на серверных и клиентских компонентах и e2e на взаимодействие через браузер. Настройка идёт через Vitest или Jest с Testing Library, e2e — через Playwright.

import { render, screen } from '@testing-library/react';
import ProductCard from './ProductCard';

test('отображает цену', () => {
  render(<ProductCard name="Наушники" price={2990} />);
  expect(screen.getByText('2990')).toBeInTheDocument();
});

Компонент, использующий серверные данные, оборачивайте в мок-хендлеры (MSW). Для e2e запускайте Playwright на продакшен-сборке next build: тест проходит «как в проде», а не по dev-серверу. В CI команды next build и тесты запускаются на каждый pull request.

Тестируйте и клиентские, и серверные компоненты: ошибка асинхронной загрузки проявляется только в real-рендере.

17. Миграция с Pages Router на App Router

Переезд целого проекта на App Router — задача плановая: обе модели могут сосуществовать в одном приложении. Начните с новых маршрутов на App Router, а старые страницы переводите постепенно. Держите глобальный layout, провайдеры и данные в единой точке.

Pages RouterApp Router
getServerSidePropsserver component + async fetch
getStaticPropsgenerateStaticParams + caching
_app.jsxroot layout.jsx
_document.jsxразметка в layout + метаданные
API routesroute handlers (app/api)

Часть страниц удобнее оставить в старом роутере до полного перехода: Next.js поддерживает гибридный режим. Переносите загрузку данных в server components, а клиентские обработчики вроде состояния формы — в компоненты с директивой 'use client'.

Кэширующие директивы (cache по умолчанию) в App Router отличаются от Pages: не переносите логику чтения «как есть», иначе данные перестанут обновляться.

18. Безопасность: headers, CSP и защита API

Заголовки безопасности настраиваются централизованно в next.config.js. Среди важнейших: frame-анcестор для защиты от clickjacking, строгий transport security (HSTS) и Content Security Policy, ограничивающий источники скриптов и стилей.

async headers() {
  return [{
    source: '/(.*)',
    headers: [
      { key: 'X-Frame-Options', value: 'DENY' },
      { key: 'Content-Security-Policy',
        value: "default-src 'self'; style-src 'self' 'unsafe-inline'" },
      { key: 'Referrer-Policy', value: 'no-referrer' }
    ]
  }];
}

Route handlers и server actions принимают запросы извне — валидируйте входные данные и права доступа на сервере, не полагаясь на скрытие полей в форме. Для NextAuth реализуйте проверку сессии в каждом защищённом маршруте и обработчике действий.

Прогоняйте конфиг через онлайн-проверку CSP и включайте отчёты о нарушениях без блокировок, чтобы сперва собрать реальную картину.

19. Пагинация, фильтры и строка поиска

Состояние списка удобно держать в URL: значения searchParams передаются серверной странице и позволяют строить рабочие ссылки навигации без клиентского состояния. При каждом переходе страница серверно перерисовывается с новыми параметрами.

export default async function Catalog({ searchParams }) {
  const { q = '', page = '1' } = await searchParams;
  const pageNum = Math.max(1, Number(page) || 1);
  const items = await getItems({ q, page: pageNum });

  const next = new URLSearchParams({ q, page: String(pageNum + 1) });
  return (
    <>
      <Link href={`/catalog?${next}`}>Дальше</Link>
      {/* список items */}
    </>
  );
}

Фильтры превращаются в ссылки с ?category=…, пагинация — в числа страницы, а кнопки «применить» не нужны вовсе: изменение URL само перерисует страницу. Такой подход работает даже без JavaScript и легко кэшируется.

Не пересчитывайте пагинацию на клиенте через useState: сброс по кнопке «Назад» и закладки сломают ожидания пользователя.

20. Формы и валидация: Server Actions с Zod

Server Actions позволяют обрабатывать формы без отдельного API-маршрута: код выполняется на сервере, где безопасно читать базу и секреты. Валидацию выносите в схему Zod и переиспользуйте её между сервером и клиентом.

import { z } from 'zod';

const schema = z.object({ email: z.string().email() });

export async function signup(prev, formData) {
  const parsed = schema.safeParse({ email: formData.get('email') });
  if (!parsed.success) return { error: parsed.error.issues[0].message };
  await createUser(parsed.data.email);   // серверный доступ к БД
  return { ok: true };
}

Ошибки возвращаются в состоянии действия и выводятся рядом с полями, а после успеха страницу обновляют через revalidatePath(). Атрибут формы action={signup} включает прогрессивную отправку даже без клиентского кода.

Тип Zod выводит тип данных автоматически — один оригинал истины для схемы, интерфейсов и документации.
Любую клиентскую проверку обходят: финальная валидация всегда серверная, а данные для неё получайте только из formData приложения.

21. Локализация с next-intl

Для многоязычного сайта используйте next-intl: файлы переводов в JSON, маршрутизация через сегмент локали и форматирование дат и чисел по правилам языка. Интеграция делается парой строк в конфиге и корневом layout.

// app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'Common' });
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

Сообщения храните в файлах messages/ru.json и messages/en.json, а доступ через функцию t() раздаёт локализованные строки. Плюс next-intl даёт правильные множественные формы и конвертацию чисел.

Храните даты по локальному формату: next-intl сам подставляет правила стран, не нужно хардкодить разделители.
Не склеивайте переводы конкатенацией: порядок слов различается между языками, и перевод ломается даже при корректных одиночных ключах.

22. On-demand ревалидация и вебхуки

Кэш Next.js можно обновлять не по расписанию, а по событию: когда CMS или внешний сервис сообщает об изменении данных через вебхук. Подход называют on-demand (ISR по событию) — он совмещает скорость статики и свежесть контента.

// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';

export async function POST(req) {
  const secret = req.headers.get('x-revalidate-secret');
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'forbidden' }, { status: 403 });
  }
  revalidateTag('products');   // обновить все страницы с этим тегом
  return Response.json({ revalidated: true });
}

Помечайте fetch запроса тегом next: { tags: ['products'] }, и при вызове маршрута пересоздаются только нужные страницы. Вебхук держите защищённым секретом — иначе любой сможет обнулить кэш и ударить по ресурсам.

Не открывайте маршрут ревалидации без аутентификации: неконтролируемый вызов превращает кэш в источник постоянной нагрузки.

Что дальше

Теперь ты знаешь, как устроен Next.js: от файловой структуры App Router и layout-ов до серверных компонентов, данных и деплоя. Собери небольшой блог с постами и тегами, чтобы закрепить динамические пути и статическую генерацию. Продолжить стоит разбором самого React, а взаимодействие с бэкендом — курсом по REST API.

← Вернуться к курсам