Next.js — фреймворк на основе React
Next.js — самый популярный React-фреймворк: роутинг, серверный рендеринг, оптимизация изображений и API-маршруты работают «из коробки». В этом уроке ты создашь приложение на App Router, разберёшься с серверными и клиентскими компонентами и задеплоишь проект на Vercel.
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:30002. 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>
)
}<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>
)
}'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 | по расписанию | каталог, новости, цены |
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: 'Отличный курс!' })
})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 → Deploy11. 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*']
};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'
});- SSG (revalidate=0) — статическая генерация, быстрейшая выдача.
- ISR — статика с фонным пересбором (revalidate > 0).
- SSR/dynamic — рендер на каждый запрос, свежесть без кэша.
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' }
};Проверяйте превью через валидатор отладчика соцсети и не забывайте про sitemap.ts и robots.ts — их поддерживают локальные файлы в папке app/.
14. Мониторинг и наблюдаемость
Для продакшена важно видеть: скорость страниц, ошибки фронта и серверные исключения. Начните с карт витальности и сбора ошибок через логирование.
| Инструмент | Назначение |
|---|---|
| Vercel Analytics | Core 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 });
}15. Параллельные и перехватывающие маршруты
App Router поддерживает продвинутые паттерны композиции страниц. Параллельные маршруты (parallel routes) показывают на одной странице несколько независимых слотов — например, таблицу и детали клика — названных папками с префиксом @.
app/
dashboard/
@stats/page.jsx
@orders/page.jsx
layout.jsxСлоты рендерятся одновременно и независимо, поэтому медленный график не блокирует остальную часть страницы. Перехватывающие маршруты с именем (.), (..) и (...) позволяют открыть модальное окно с содержимым другого маршрута, сохранив адресную строку прежней — классический UX карточки товара поверх каталога.
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.
- Дата-атрибуты data-testid — соглашение для стабильных локаторов.
- Мокайте время и сеть, чтобы тесты не зависели от часового пояса и внешних API.
- Несколько смоук-тестов главных маршрутов важнее сотни хрупких деталей.
17. Миграция с Pages Router на App Router
Переезд целого проекта на App Router — задача плановая: обе модели могут сосуществовать в одном приложении. Начните с новых маршрутов на App Router, а старые страницы переводите постепенно. Держите глобальный layout, провайдеры и данные в единой точке.
| Pages Router | App Router |
|---|---|
| getServerSideProps | server component + async fetch |
| getStaticProps | generateStaticParams + caching |
| _app.jsx | root layout.jsx |
| _document.jsx | разметка в layout + метаданные |
| API routes | route handlers (app/api) |
Часть страниц удобнее оставить в старом роутере до полного перехода: Next.js поддерживает гибридный режим. Переносите загрузку данных в server components, а клиентские обработчики вроде состояния формы — в компоненты с директивой 'use client'.
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 реализуйте проверку сессии в каждом защищённом маршруте и обработчике действий.
- Проверяйте каждый server action на принадлежность действия текущему пользователю.
- Не пишите секреты в NEXT_PUBLIC_ переменные — они попадают в клиентский бандл.
- Логируйте попытки доступа и аномалии через middleware для быстрого реагирования.
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 и легко кэшируется.
- Приводите параметры к безопасным значениям: числа через Number(), строки экранируйте при выводе.
- Не доверяйте сторонним значениям: строгие проверки форматирования защищают от невалидных ключей.
- Для длинных форм переходите на server actions и ревалидацию — об этом в следующих разделах.
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} включает прогрессивную отправку даже без клиентского кода.
- Проверяйте не только формат, но и типы и длину полей.
- Не доверяйте полям типа «hidden»: сервер читает только 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 даёт правильные множественные формы и конвертацию чисел.
- Включайте статическую генерацию для каждого locale через generateStaticParams.
- Канонические метки и hreflang стройте на базе локали, чтобы не удваивать страницы в поиске.
- Строки в атрибутах и плейсхолдерах выводите тем же способом, что и видимые тексты.
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'] }, и при вызове маршрута пересоздаются только нужные страницы. Вебхук держите защищённым секретом — иначе любой сможет обнулить кэш и ударить по ресурсам.
- Теги удобнее путей для групп страниц: одно событие обновляет целый каталог.
- Ставьте rate-limit на вебхук, чтобы всплеск уведомлений не выжег CPU.
- Проверяйте подпись запроса (HMAC), а не только секретный заголовок.
Что дальше
Теперь ты знаешь, как устроен Next.js: от файловой структуры App Router и layout-ов до серверных компонентов, данных и деплоя. Собери небольшой блог с постами и тегами, чтобы закрепить динамические пути и статическую генерацию. Продолжить стоит разбором самого React, а взаимодействие с бэкендом — курсом по REST API.