Модуль 1
Введение и основы Next.js
- Что такое Next.js
- Установка
- Структура проекта
- Файловая маршрутизация
#1.1. Что такое Next.js и зачем он нужен
Next.js — это React-фреймворк для создания полнофункциональных веб-приложений. Он предоставляет инструменты и конфигурацию, необходимые для React, и добавляет собственные возможности: серверный рендеринг, статическую генерацию, файловую маршрутизацию, оптимизацию изображений и многое другое. На момент написания курса актуальная версия — Next.js 16, в её составе React 19.2.
#Почему не просто React (CRA / Vite)?
Чистый React (с Vite или Create React App) — это SPA (Single Page Application). Всё рендерится на клиенте. Это создаёт проблемы:
- SEO — поисковые роботы видят пустой HTML
- Первая загрузка — пользователь ждёт, пока скачается весь JS-бандл
- Нет серверной логики — нужен отдельный бэкенд
- Маршрутизация — нужно устанавливать react-router вручную
Next.js решает все эти проблемы из коробки:
- SSR/SSG — HTML генерируется на сервере, SEO работает
- Быстрая загрузка — пользователь сразу видит контент
- Fullstack — серверная логика в самом проекте (Server Actions, API Routes)
- Файловая маршрутизация — создание файла = создание страницы
- Оптимизации — изображения, шрифты, код-сплиттинг автоматически
#Сравнительная таблица
| Возможность | CRA/Vite | Next.js (Pages) | Next.js (App) |
|---|---|---|---|
| Серверный рендеринг | ❌ | ✅ getServerSideProps | ✅ По умолчанию |
| Статическая генерация | ❌ | ✅ getStaticProps | ✅ По умолчанию |
| Файловая маршрутизация | ❌ | ✅ pages/ | ✅ app/ |
| Server Components | ❌ | ❌ | ✅ |
| Server Actions | ❌ | ❌ | ✅ |
| Streaming / Suspense | ❌ | Частично | ✅ |
#1.2. Установка и создание проекта
#Предварительные требования
- Node.js 24 LTS (сам Next.js 16 запускается и на 20.9, но демо курса рассчитаны на 24)
- npm, yarn или pnpm (в курсе используется npm)
- Редактор кода (рекомендуется VS Code)
- Windows: скачайте установщик с https://nodejs.org (LTS-кнопка). После установки перезапустите терминал.
- macOS: либо тот же установщик, либо Homebrew: brew install node@24 (нужен Homebrew — brew.sh). Рекомендуется nvm: brew install nvm, затем nvm install --lts.
- Ubuntu/Linux: рекомендуется nvm. Команды: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash → перезапустите терминал → nvm install --lts.
nvm удобен тем, что позволяет переключаться между версиями Node — это пригодится, когда у разных проектов разные требования.
Проверьте установку Node.js:
node --version
# Должно быть v24 или выше
npm --version#Как открыть терминал на вашей ОС
Перед практикой убедитесь, что умеете открывать терминал и переходить в нужную папку. Это базовый навык — без него дальнейшая работа невозможна.
- Windows (рекомендуется PowerShell): нажмите Win → начните вводить «PowerShell» → откройте «Windows PowerShell». Альтернатива — Win+R, ввести cmd, Enter (классический cmd.exe).
- Windows Terminal (новое приложение от Microsoft): Microsoft Store → найти «Windows Terminal». Он совмещает PowerShell, cmd и WSL в одной программе с вкладками — рекомендуется установить.
- macOS: Cmd+Space → начните вводить «Terminal» → Enter. Или Finder → Программы → Утилиты → Терминал.
- Ubuntu/Linux: Ctrl+Alt+T (стандартное сочетание). Альтернатива — найти «Terminal» в меню приложений.
Чтобы перейти в нужную папку, используйте команду cd (от англ. «change directory»):
# Windows (PowerShell или cmd): обратные слеши работают, но прямые тоже
cd C:\Users\Username\Projects
cd C:/Users/Username/Projects # тоже валидно
# macOS / Linux: только прямые слеши
cd ~/Projects # ~ означает домашнюю папку
cd /Users/username/Projects # macOS
cd /home/username/Projects # LinuxПроверьте, в какой папке вы сейчас находитесь:
# Windows (PowerShell)
pwd
# Windows (cmd)
cd
# macOS / Linux
pwd#1.3. Практика: создаём проект
Шаг 1. Откройте терминал и выполните команду:
npx create-next-app@16.3.0 my-first-nextjs-appШаг 2. Ответьте на вопросы установщика:
✔ What is your project named? → my-first-nextjs-app
✔ Would you like to use the recommended Next.js defaults? → No, customize settings
✔ Would you like to use TypeScript? → Yes
✔ Which linter would you like to use? → ESLint
✔ Would you like to use React Compiler? → No
✔ Would you like to use Tailwind CSS? → Yes
✔ Would you like your code inside a `src/` directory? → No
✔ Would you like to use App Router? (recommended) → Yes
✔ Would you like to customize the import alias (`@/*` by default)? → No
✔ Would you like to include AGENTS.md to guide coding agents? → Yes- TypeScript: безопаснее JavaScript, выявляет ошибки на этапе написания кода. В индустрии для Next.js это стандарт.
- ESLint: проверяет код на типичные ошибки. Biome — современная альтернатива (быстрее), но в курсе используется ESLint, поскольку большинство примеров в интернете рассчитано на него.
- React Compiler: автоматически оптимизирует ре-рендеры. Стабилен в Next.js 16, но на этом этапе отключается — так проще осваивать базовые концепции. Будет включён позже, в модуле про оптимизацию.
- Tailwind CSS: utility-first CSS-фреймворк. В Next.js 16 поставляется Tailwind v4 — конфигурация теперь задаётся в CSS-файле, а не в tailwind.config.ts.
- Директория src/: без неё проще — папка app/ находится сразу в корне.
- App Router: единственный современный путь. Pages Router в курсе не используется.
- Алиас @/*: позволяет писать import Foo from "@/components/Foo" вместо длинного относительного пути.
- AGENTS.md: файл с подсказками для AI-инструментов (Copilot, Cursor, Claude). К работе Next.js не относится — при желании его можно удалить.
Шаг 3. Перейдите в папку проекта и запустите:
cd my-first-nextjs-app
npm run devШаг 4. Откройте браузер:
Перейдите по адресу http://localhost:3000. Откроется стартовая страница Next.js с логотипом и ссылками на документацию.
#Если что-то пошло не так (типовые ошибки)
- Проверьте подключение к интернету (особенно в колледже — там бывают проблемы с прокси/сертификатами, см. страницу «Перед началом работы»).
- Прервите процесс (Ctrl+C), удалите созданную папку, запустите команду заново.
- Если проблема в SSL — выполните: npm config set strict-ssl false (только для учебных целей!).
- Значит, npm пытается писать в системную папку без прав. Лучшее решение — использовать nvm (см. блок выше): он устанавливает Node в домашнюю папку.
- Никогда не запускайте npm с sudo — это в дальнейшем портит права доступа к файлам.
- Команда запускается не из папки проекта. Проверьте: cd my-first-nextjs-app, затем снова npm run dev.
- Проверьте, что сервер в терминале всё ещё запущен и слушает порт (должна быть строка «Local: http://localhost:3000»).
- Если порт 3000 занят — Next.js выведет сообщение «Port 3000 is in use, trying 3001 instead». Откройте указанный в терминале адрес.
- Брандмауэр Windows при первом запуске может запросить разрешение — предоставьте его для частной сети.
- Вероятно, зависимости установлены не полностью. Запустите npm install в папке проекта.
#1.4. Структура проекта
После создания проекта структура файлов выглядит так:
my-first-nextjs-app/
├── app/ # Главная папка приложения (App Router)
│ ├── favicon.ico # Иконка сайта
│ ├── globals.css # Глобальные стили
│ ├── layout.tsx # Корневой layout (обёртка для всех страниц)
│ └── page.tsx # Главная страница (/)
├── public/ # Статические файлы (картинки, шрифты)
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── AGENTS.md # Подсказки AI-инструментам (можно удалить)
├── CLAUDE.md # То же для Claude Code (можно удалить)
├── README.md # Описание проекта
├── eslint.config.mjs # Конфигурация ESLint (flat config)
├── .gitignore
├── next.config.ts # Конфигурация Next.js (TypeScript)
├── package.json # Зависимости и скрипты
├── postcss.config.mjs # Конфигурация PostCSS (для Tailwind v4)
└── tsconfig.json # Конфигурация TypeScript#Ключевые файлы и папки
app/ — ключевая папка приложения. Здесь находятся все страницы, layouts и компоненты.
public/ — статические файлы. Доступны по URL напрямую: public/logo.png → http://localhost:3000/logo.png
next.config.ts — конфигурация Next.js (редиректы, env-переменные, плагины).
package.json — скрипты: dev, build, start, lint
#Скрипты в package.json
{
"scripts": {
"dev": "next dev", // Запуск dev-сервера (Turbopack теперь по умолчанию)
"build": "next build", // Сборка для продакшена
"start": "next start", // Запуск продакшен-сервера
"lint": "eslint" // Проверка кода ESLint (next lint в v16 удалён)
}
}Разница между dev и build + start:
- npm run dev — режим разработки, горячая перезагрузка, подробные сообщения об ошибках
- npm run build — создаёт оптимизированную сборку в папке .next/
- npm run start — запускает собранное приложение (как на сервере)
#1.5. App Router vs Pages Router
В Next.js существуют два подхода к маршрутизации:
| Pages Router (старый) | App Router (новый) | |
|---|---|---|
| Папка | pages/ | app/ |
| Компоненты | Только клиентские | Серверные по умолчанию |
| Data fetching | getServerSideProps и др. | async-компоненты + fetch |
| Layouts | Ручные (_app.tsx) | Вложенные layout.tsx |
| Loading/Error | Ручные | loading.tsx, error.tsx |
| Server Actions | ❌ | ✅ |
#1.6. Файловая маршрутизация
В Next.js App Router каждая папка внутри app/ — это сегмент URL. Файл page.tsx внутри папки делает этот сегмент доступным как страницу.
#Базовые маршруты
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── contact/
│ └── page.tsx → /contact
└── blog/
└── page.tsx → /blog#Специальные файлы
app/
├── layout.tsx # Обёртка (сохраняется при навигации)
├── page.tsx # Уникальный контент страницы
├── loading.tsx # UI загрузки (автоматический Suspense)
├── error.tsx # Обработка ошибок
├── not-found.tsx # Страница 404
└── template.tsx # Как layout, но пересоздаётся при навигацииlayout.tsx — оборачивает дочерние страницы. Не перерисовывается при навигации между дочерними маршрутами. Применяется для шапки, сайдбара, футера.
page.tsx — содержимое конкретной страницы. Без этого файла маршрут не будет доступен.
loading.tsx — показывается автоматически, пока page.tsx загружает данные.
error.tsx — перехватывает ошибки в дочерних компонентах (Error Boundary).
not-found.tsx — показывается при вызове notFound() или при отсутствии маршрута.
#1.7. Практика: создаём страницы
Шаг 1. Замените содержимое файла app/page.tsx:
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Главная страница</h1>
<p>Добро пожаловать в мой первый Next.js проект!</p>
</div>
);
}Шаг 2. Создайте папку app/about/ и файл page.tsx:
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>О нас</h1>
<p>Это страница «О нас». URL: /about</p>
</div>
);
}Шаг 3. Создайте папку app/contact/ и файл page.tsx:
// app/contact/page.tsx
export default function ContactPage() {
return (
<div>
<h1>Контакты</h1>
<p>Напишите нам: hello@example.com</p>
</div>
);
}Шаг 4. Проверьте в браузере:
- http://localhost:3000 → Главная страница
- http://localhost:3000/about → О нас
- http://localhost:3000/contact → Контакты
- http://localhost:3000/anything → 404 (страница не найдена)
#1.8. Вложенные маршруты и layout.tsx
#Вложенные маршруты
app/
└── blog/
├── page.tsx → /blog
└── [slug]/
└── page.tsx → /blog/my-first-postПрактика: блог с вложенными маршрутами
Шаг 1. Создайте app/blog/page.tsx:
// app/blog/page.tsx
import Link from "next/link";
export default function BlogPage() {
const posts = [
{ slug: "first-post", title: "Первый пост" },
{ slug: "second-post", title: "Второй пост" },
{ slug: "nextjs-intro", title: "Введение в Next.js" },
];
return (
<div>
<h1>Блог</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}Шаг 2. Создайте app/blog/[slug]/page.tsx:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<div>
<h1>Пост: {slug}</h1>
<p>Это динамическая страница для поста с slug: {slug}</p>
</div>
);
}Шаг 3. Проверьте в браузере:
- http://localhost:3000/blog → Список постов
- http://localhost:3000/blog/first-post → Пост: first-post
- http://localhost:3000/blog/anything → Пост: anything
#Layout — общая обёртка
layout.tsx оборачивает все дочерние страницы. Корневой layout (app/layout.tsx) обязателен и содержит <html> и <body>.
Шаг 4. Отредактируйте app/layout.tsx:
// app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = {
title: "Мой Next.js сайт",
description: "Учебный проект",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<nav style={{ padding: "1rem", borderBottom: "1px solid #ccc" }}>
<Link href="/" style={{ marginRight: "1rem" }}>Главная</Link>
<Link href="/about" style={{ marginRight: "1rem" }}>О нас</Link>
<Link href="/blog" style={{ marginRight: "1rem" }}>Блог</Link>
<Link href="/contact">Контакты</Link>
</nav>
<main style={{ padding: "2rem" }}>{children}</main>
</body>
</html>
);
}Шаг 5. Проверьте в браузере:
Теперь на каждой странице есть навигация сверху. При переходе между страницами навигация не перерисовывается — за такое поведение отвечает layout.
#Вложенный layout для блога
Шаг 6. Создайте app/blog/layout.tsx:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div style={{ display: "flex", gap: "2rem" }}>
<aside style={{
width: "200px",
borderRight: "1px solid #ccc",
paddingRight: "1rem",
}}>
<h3>Категории</h3>
<ul>
<li>JavaScript</li>
<li>React</li>
<li>Next.js</li>
</ul>
</aside>
<div style={{ flex: 1 }}>{children}</div>
</div>
);
}Шаг 7. Проверьте:
Перейдите на /blog — слева появится сайдбар с категориями. Перейдите на /blog/first-post — сайдбар остаётся. Перейдите на /about — сайдбара нет, потому что он определён только для /blog/*.
#1.9. Группы маршрутов и динамические сегменты
#Группы маршрутов (Route Groups)
Папки в круглых скобках (group) не влияют на URL, но позволяют организовать код:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about (НЕ /marketing/about!)
│ └── pricing/
│ └── page.tsx → /pricing
├── (dashboard)/
│ ├── layout.tsx # Отдельный layout для дашборда
│ ├── settings/
│ │ └── page.tsx → /settings
│ └── profile/
│ └── page.tsx → /profile
└── layout.tsxЭто полезно, когда у разных разделов сайта разные layouts (например, у маркетинговых страниц и дашборда).
#Динамические сегменты
// Один сегмент
app/blog/[slug]/page.tsx → /blog/my-post
// params: { slug: "my-post" }
// Несколько сегментов
app/shop/[category]/[id]/page.tsx → /shop/electronics/42
// params: { category: "electronics", id: "42" }
// Catch-all (все оставшиеся сегменты)
app/docs/[...slug]/page.tsx → /docs/a/b/c
// params: { slug: ["a", "b", "c"] }
// Optional catch-all
app/docs/[[...slug]]/page.tsx → /docs ИЛИ /docs/a/b/c
// params: { slug: undefined } ИЛИ { slug: ["a", "b", "c"] }#1.10. Практика: магазин с категориями
Шаг 1. Создайте app/shop/page.tsx:
// app/shop/page.tsx
import Link from "next/link";
export default function ShopPage() {
const categories = ["electronics", "clothing", "books"];
return (
<div>
<h1>Магазин</h1>
<ul>
{categories.map((cat) => (
<li key={cat}>
<Link href={`/shop/${cat}`}>{cat}</Link>
</li>
))}
</ul>
</div>
);
}Шаг 2. Создайте app/shop/[category]/page.tsx:
// app/shop/[category]/page.tsx
import Link from "next/link";
export default async function CategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const { category } = await params;
const products = [
{ id: "1", name: `${category} товар 1` },
{ id: "2", name: `${category} товар 2` },
{ id: "3", name: `${category} товар 3` },
];
return (
<div>
<h1>Категория: {category}</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link href={`/shop/${category}/${product.id}`}>
{product.name}
</Link>
</li>
))}
</ul>
</div>
);
}Шаг 3. Создайте app/shop/[category]/[id]/page.tsx:
// app/shop/[category]/[id]/page.tsx
export default async function ProductPage({
params,
}: {
params: Promise<{ category: string; id: string }>;
}) {
const { category, id } = await params;
return (
<div>
<h1>Товар #{id}</h1>
<p>Категория: {category}</p>
<p>Здесь будет подробная информация о товаре.</p>
</div>
);
}Шаг 4. Проверьте:
- http://localhost:3000/shop → Список категорий
- http://localhost:3000/shop/electronics → Товары в категории
- http://localhost:3000/shop/electronics/1 → Страница товара
#1.11. loading.tsx, error.tsx, not-found.tsx
#loading.tsx — индикатор загрузки
Шаг 1. Создайте app/blog/loading.tsx:
// app/blog/loading.tsx
export default function Loading() {
return (
<div style={{ padding: "2rem", textAlign: "center" }}>
<p>⏳ Загрузка постов...</p>
</div>
);
}Шаг 2. Добавьте искусственную задержку в app/blog/page.tsx:
// app/blog/page.tsx
import Link from "next/link";
async function getPosts() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return [
{ slug: "first-post", title: "Первый пост" },
{ slug: "second-post", title: "Второй пост" },
];
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Блог</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}Шаг 3. Проверьте:
Перейдите на /blog — на 2 секунды появится «⏳ Загрузка постов...», затем отобразится список. Next.js автоматически оборачивает page.tsx в <Suspense> с fallback из loading.tsx.
#error.tsx — обработка ошибок
Шаг 4. Создайте app/blog/error.tsx:
// app/blog/error.tsx
"use client"; // error.tsx ОБЯЗАТЕЛЬНО должен быть клиентским!
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<div style={{ padding: "2rem", color: "red" }}>
<h2>Ошибка!</h2>
<p>{error.message}</p>
<button onClick={() => retry()}>Попробовать снова</button>
</div>
);
}Функцию retry Next.js передаёт в error.tsx сам: она заново запрашивает у сервера упавший сегмент и перерисовывает его. Если причина ошибки была разовой (не ответил внешний сервис, база перезапускалась), повтор покажет страницу. Если ошибка в коде, повтор снова закончится ошибкой, и блок останется на месте.
Шаг 5. Проверьте работу error.tsx:
Для этого временно вызовите ошибку на странице блога. В файле app/blog/page.tsx добавьте выброс ошибки первой строкой функции getPosts:
throw new Error("Тестовая ошибка загрузки постов");Обновите http://localhost:3000/blog — вместо списка постов появится красный блок «Ошибка!» с текстом ошибки и кнопкой «Попробовать снова». Значит, сработал Error Boundary из error.tsx: он перехватил ошибку дочерней страницы и показал запасной интерфейс. Убедившись, что блок работает, удалите строку с throw — иначе страница блога так и будет завершаться ошибкой.
#not-found.tsx — страница 404
Шаг 6. Создайте app/not-found.tsx:
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div style={{ padding: "2rem", textAlign: "center" }}>
<h1>404 — Страница не найдена</h1>
<p>Запрашиваемая страница не существует.</p>
<Link href="/">Вернуться на главную</Link>
</div>
);
}Шаг 7. Проверьте:
Перейдите на http://localhost:3000/nonexistent — откроется созданная вами страница 404.
#Итоги модуля 1
- Next.js — React-фреймворк с SSR, SSG, файловой маршрутизацией
- App Router — современный подход (папка app/)
- page.tsx — создаёт страницу, layout.tsx — общая обёртка
- Динамические сегменты: [slug], [...slug], [[...slug]]
- Группы маршрутов: (group) — не влияют на URL
- loading.tsx, error.tsx, not-found.tsx — автоматическая обработка состояний
#Домашнее задание
Все задания выполняются в одном новом проекте — сайте-афише городских мероприятий; данные берутся из обычного массива в коде, работа с сервером и базой появится в следующих модулях.
- Создайте проект командой create-next-app с теми же ответами установщика, что и в практике 1.3, и соберите каркас афиши: корневой layout.tsx с шапкой-навигацией и подвалом, главную страницу с приветствием и тремя ближайшими событиями и страницу /events со списком всех событий; сам массив событий (идентификатор, название, дата, площадка, описание) вынесите в файл lib/events.ts и импортируйте его на страницах.
- Добавьте разделу событий собственную обёртку и карточки: файл app/events/layout.tsx с заголовком раздела и боковой колонкой фильтров-заглушек, а внутри — динамический маршрут app/events/[id]/page.tsx, который находит событие по идентификатору из params (в Next.js 16 params — это Promise, его распаковывают через await) и выводит подробную карточку.
- Оформите служебные страницы через группу маршрутов: папка (info) со страницами help и rules — адреса должны остаться /help и /rules, без сегмента info; положите в группу собственный layout.tsx с навигацией по служебным страницам и убедитесь, что на страницах событий он не появляется.
- Добавьте разделу событий состояния загрузки и ошибки: loading.tsx со скелетоном списка (чтобы его увидеть, вставьте в страницу искусственную задержку await new Promise((resolve) => setTimeout(resolve, 1500))) и error.tsx с текстом ошибки и кнопкой повтора, вызывающей retry; для проверки error.tsx временно бросьте исключение в странице события. Файл loading.tsx положите так, чтобы он накрывал только список, а не весь раздел: если он окажется прямо в app/events, то будет действовать и на страницу отдельного события, а тогда следующий пункт задания перестанет работать — ответ начинает уходить браузеру до вызова notFound(), и вместо кода 404 несуществующее событие вернёт 200. Простое решение — завести для списка группу маршрутов, например app/events/(list), и держать page.tsx и loading.tsx внутри неё: адрес /events от этого не изменится.
- Закройте несуществующие адреса: собственная страница app/not-found.tsx со ссылкой на главную, а в app/events/[id]/page.tsx — вызов notFound() из next/navigation, когда события с таким идентификатором в массиве нет.
- (со звёздочкой) Добавьте раздел справки со свободной вложенностью на необязательном catch-all сегменте app/docs/[[...slug]]/page.tsx: по адресу /docs открывается оглавление, по /docs/tickets — статья, по /docs/tickets/refund — вложенная статья, а для неизвестного пути вызывается notFound(); хлебные крошки соберите из массива сегментов, а сам раздел оберните отдельным вложенным layout.tsx с боковым меню.