Перейти к содержимому

2. App Router vs Pages Router

Это, пожалуй, самый важный выбор при работе с Next.js сегодня. Два поколения системы маршрутизации, два разных подхода к архитектуре. Давай разберём всё по-честному! 🎯

Next.js 1-12 Next.js 13+
──────────────────── ────────────────────────────
pages/ app/
index.tsx → page.tsx
about.tsx → about/page.tsx
blog/ blog/
[slug].tsx → [slug]/page.tsx
_app.tsx → layout.tsx
_document.tsx → layout.tsx (root)
api/ → api/ (route.ts)

Pages Router появился в Next.js 1.0 и работал вплоть до Next.js 12. Это проверенная система, которую знают миллионы разработчиков:

// pages/index.tsx — главная страница
export default function HomePage({ posts }) {
return (
<div>
<h1>Мой блог</h1>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
// getServerSideProps — данные при каждом запросе (SSR)
export async function getServerSideProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return { props: { posts } };
}
// pages/blog/[slug].tsx — динамический роут
export default function BlogPost({ post }) {
return <article>{post.content}</article>;
}
// getStaticProps — данные при сборке (SSG)
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return { props: { post }, revalidate: 60 }; // ISR!
}
// getStaticPaths — какие slug генерировать при сборке
export async function getStaticPaths() {
const posts = await getAllPosts();
return {
paths: posts.map(p => ({ params: { slug: p.slug } })),
fallback: 'blocking'
};
}
// pages/_app.tsx — глобальная обёртка
import type { AppProps } from 'next/app';
import '../styles/globals.css';
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthProvider>
);
}

App Router построен на React Server Components и меняет всё:

// app/page.tsx — главная страница
// Server Component по умолчанию — нет 'use client'!
export default async function HomePage() {
// Данные прямо в компоненте, без getServerSideProps!
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<div>
<h1>Мой блог</h1>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
// app/blog/[slug]/page.tsx — динамический роут
export default async function BlogPost({
params
}: {
params: Promise<{ slug: string }> // Next.js 15: params async!
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
// Больше нет getStaticPaths — используем generateStaticParams
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
// ISR через экспорт переменной
export const revalidate = 60;
// app/layout.tsx — root layout (заменяет _app.tsx и _document.tsx)
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Мой блог',
description: 'Описание сайта',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<AuthProvider>
<Header />
<main>{children}</main>
<Footer />
</AuthProvider>
</body>
</html>
);
}

В App Router каждый файл имеет специальное назначение:

app/
├── layout.tsx # Обёртка — остаётся при навигации
├── template.tsx # Обёртка — пересоздаётся при навигации
├── page.tsx # Страница — отображается по URL
├── loading.tsx # Skeleton/spinner во время загрузки
├── error.tsx # UI ошибки (Error Boundary)
├── not-found.tsx # UI для 404
├── global-error.tsx # Ошибки в root layout
└── route.ts # API endpoint (Route Handler)

Подробнее о каждом:

// app/dashboard/layout.tsx — layout для /dashboard и всех дочерних роутов
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<DashboardSidebar />
<main>{children}</main>
</div>
);
}
// app/dashboard/loading.tsx — автоматически показывается пока page.tsx грузится
export default function DashboardLoading() {
return (
<div className="skeleton">
<div className="skeleton-title" />
<div className="skeleton-line" />
<div className="skeleton-line" />
</div>
);
}
// Next.js автоматически оборачивает page.tsx в <Suspense fallback={<DashboardLoading />}>
// app/dashboard/error.tsx — обработчик ошибок
'use client'; // Error boundaries должны быть Client Components!
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Что-то пошло не так!</h2>
<p>{error.message}</p>
<button onClick={reset}>Попробовать снова</button>
</div>
);
}
// app/blog/[slug]/not-found.tsx — показывается при вызове notFound()
import Link from 'next/link';
export default function PostNotFound() {
return (
<div>
<h1>Пост не найден 😔</h1>
<p>Этот пост был удалён или никогда не существовал.</p>
<Link href="/blog">← Вернуться в блог</Link>
</div>
);
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound(); // Вызовет not-found.tsx
return <article>{post.content}</article>;
}

ФичаPages RouterApp Router
Папкаpages/app/
Данные SSRgetServerSidePropsasync компонент + fetch
Данные SSGgetStaticPropsasync компонент + generateStaticParams
ISRrevalidate в getStaticPropsexport const revalidate = N
Layout_app.tsx (один на всё)layout.tsx (вложенные!)
Loading UIВручную с useStateloading.tsx автоматически
Error UIВручнуюerror.tsx автоматически
Server Components✅ По умолчанию
Server Actions
Стриминг
Metadata APIВручную через Headexport const metadata
Параллельные роуты
Перехватывающие роуты

Хорошая новость: можно использовать оба роутера одновременно! Это полезно для постепенной миграции:

my-app/
├── app/ # Новые страницы — App Router
│ ├── layout.tsx
│ ├── about/
│ │ └── page.tsx # /about → App Router
│ └── dashboard/
│ └── page.tsx # /dashboard → App Router
├── pages/ # Старые страницы — Pages Router
│ ├── _app.tsx
│ ├── index.tsx # / → Pages Router
│ └── blog/
│ └── [slug].tsx # /blog/:slug → Pages Router

Правила сосуществования:

  • App Router имеет приоритет над Pages Router для одинаковых роутов
  • Нельзя иметь app/about/page.tsx и pages/about.tsx одновременно
  • _app.tsx и _document.tsx в Pages Router НЕ влияют на App Router

App Router (рекомендуется для новых проектов):

// ✅ Используй App Router если:
// 1. Новый проект
// 2. Нужны Server Components для производительности
// 3. Хочешь Server Actions вместо API routes
// 4. Нужны вложенные layouts
// 5. Нужен streaming/Suspense
// Пример: полноценный Server Component
// app/products/page.tsx
async function ProductsPage() {
const products = await db.product.findMany(); // Прямо в компоненте!
return <ProductGrid products={products} />;
}

Pages Router (если…):

// ⚠️ Оставайся на Pages Router если:
// 1. Большой легаси-проект — миграция дорого стоит
// 2. Используешь библиотеки несовместимые с RSC
// 3. Команда не готова изучать новую парадигму
// 4. Нужна стабильность: Pages Router более зрелый
// pages/products.tsx
export default function ProductsPage({ products }) {
return <ProductGrid products={products} />;
}
export async function getServerSideProps() {
const products = await db.product.findMany();
return { props: { products } };
}

Это одна из лучших фич App Router! Каждый сегмент может иметь свой layout:

// app/layout.tsx — root layout (всё приложение)
export default function RootLayout({ children }) {
return (
<html lang="ru">
<body>
<GlobalNav />
{children}
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.tsx — layout для /dashboard/*
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-container">
<DashboardSidebar />
<div className="dashboard-content">
{children}
</div>
</div>
);
}
// app/dashboard/settings/layout.tsx — layout для /dashboard/settings/*
export default function SettingsLayout({ children }) {
return (
<div>
<SettingsTabs />
{children}
</div>
);
}
// app/dashboard/settings/profile/page.tsx
// Получит: RootLayout > DashboardLayout > SettingsLayout > ProfilePage

Результирующее дерево компонентов:

<RootLayout> ← app/layout.tsx
<DashboardLayout> ← app/dashboard/layout.tsx
<SettingsLayout> ← app/dashboard/settings/layout.tsx
<ProfilePage> ← app/dashboard/settings/profile/page.tsx
</SettingsLayout>
</DashboardLayout>
</RootLayout>

App Router — будущее Next.js. Если начинаешь новый проект — используй его.

Pages Router — всё ещё поддерживается и отлично работает для существующих проектов.

Ключевые файловые конвенции App Router:

  • page.tsx — страница
  • layout.tsx — обёртка (персистентная)
  • loading.tsx — UI загрузки
  • error.tsx — UI ошибки
  • not-found.tsx — UI 404
  • route.ts — API endpoint