Към блога
Блог19 март 2026 г.

App Router vs Pages Router в Next.js - Миграция и Какво да Очакваш

Stanchev Digital
App Router vs Pages Router в Next.js - Миграция и Какво да Очакваш
Имах проект - среден по размер e-commerce сайт, изграден с Next.js 12 и Pages Router. Работеше. Бързо беше. Екипът го познаваше добре. И въпреки това, в даден момент дойде неизбежният въпрос: трябва ли да мигрираме към App Router? Отговорът не беше прост. Не беше "да" или "не" - беше "зависи от много неща, и трябва да ги разбереш преди да докоснеш каквото и да е". Именно тази статия е написана за теб, ако се намираш в същата ситуация - или ако изграждаш нов проект и се чудиш кое е правилното начало.

Кратка история - откъде идваме

Pages Router съществува от самото начало на Next.js. Моделът е прост и интуитивен: всеки файл в pages/ директорията автоматично става route. pages/about.tsx е /about. pages/blog/[slug].tsx е /blog/:slug. Data fetching се случва чрез специални функции - getServerSideProps за SSR, getStaticProps за SSG, getStaticPaths за динамични статични routes. Моделът работи отлично в продължение на години. Лесен за разбиране, предвидим, с огромна документация и ecosystem. App Router е представен с Next.js 13 и стабилизиран в 13.4. Той е пълно преосмисляне на routing архитектурата, базирано на React Server Components, нови file conventions и вградена поддръжка за Suspense, streaming и layouts. В Next.js 16 App Router е зрял, production-ready и е посоката, в която се движи целият framework.

Ключови разлики между двата подхода

Преди да влезем в плюсовете и минусите, важно е да разбереш архитектурните разлики - защото те определят всичко останало. В Pages Router всеки компонент е Client Component по подразбиране. Data fetching е отделен от компонента - чрез getServerSideProps или getStaticProps, които се изпълняват на сървъра и предават данните като props. Layouts се управляват ръчно чрез _app.tsx и _document.tsx. В App Router всеки компонент е Server Component по подразбиране. Data fetching се случва директно в компонента чрез async/await. Layouts са вградена file convention - файл layout.tsx автоматично обвива всички routes в директорията. Добавят се и нови специални файлове: loading.tsx, error.tsx, not-found.tsx.
// Pages Router структура
pages/
  _app.tsx          // Global layout и providers
  _document.tsx     // HTML структура
  index.tsx         // /
  about.tsx         // /about
  blog/
    index.tsx       // /blog
    [slug].tsx      // /blog/:slug
  api/
    posts.ts        // /api/posts

// App Router структура
app/
  layout.tsx        // Root layout (задължителен)
  page.tsx          // /
  about/
    page.tsx        // /about
  blog/
    layout.tsx      // Layout за всички blog routes
    page.tsx        // /blog
    [slug]/
      page.tsx      // /blog/:slug
      loading.tsx   // Loading UI за този route
      error.tsx     // Error UI за този route
  api/
    posts/
      route.ts      // /api/posts (Route Handlers)

Плюсове на App Router

React Server Components по подразбиране

Най-голямото предимство е вече разгледано подробно в отделна статия - Server Components означават по-малко JavaScript към клиента, директен database достъп в компонентите и значително по-добри Core Web Vitals. В Pages Router всеки компонент е клиентски, а data fetching е изкуствено отделен в специални функции. В App Router компонентът и данните му са едно цяло.
Tsx
// Pages Router - data fetching е отделен от компонента
export default function ProductPage({ product }) {
  return <div>{product.name}</div>
}

export async function getServerSideProps({ params }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })
  return { props: { product } }
}

// App Router - данните живеят в компонента
export default async function ProductPage({ params }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })
  return <div>{product.name}</div>
}

Вградени Nested Layouts

В Pages Router, ако искаш различен layout за различни секции на сайта, трябва да го управляваш ръчно в _app.tsx с условна логика. В App Router layouts са file convention - просто създаваш layout.tsx файл в директорията и той автоматично се прилага за всички routes вътре.
Tsx
// app/dashboard/layout.tsx
// Този layout се прилага за /dashboard, /dashboard/settings, /dashboard/profile и т.н.

import { Sidebar } from '@/components/Sidebar'
import { requireAuth } from '@/lib/auth'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  await requireAuth() // Auth проверка за всички dashboard routes наведнъж

  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1 p-6">{children}</main>
    </div>
  )
}
Резултатът: auth логиката, sidebar-ът и структурата се дефинират веднъж и се прилагат автоматично за целия dashboard. Без повтарящ се код, без условна логика в _app.tsx.

Streaming и Suspense

App Router има вградена поддръжка за React Suspense и HTTP streaming. Това означава, че бавни части на страницата могат да се зареждат прогресивно - браузърът получава и рендира HTML на части, без да чака всичко да е готово.
Tsx
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { StatsSkeleton } from '@/components/skeletons'

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* RevenueChart е бавен - зарежда се независимо */}
      <Suspense fallback={<StatsSkeleton />}>
        <RevenueChart />
      </Suspense>

      {/* RecentOrders не чака RevenueChart */}
      <Suspense fallback={<StatsSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}
В Pages Router трябваше да чакаш getServerSideProps да завърши изцяло преди HTML-ът да тръгне към клиента. В App Router бързите части пристигат веднага, бавните - когато са готови.

Server Actions

Server Actions са може би най-радикалната промяна. Те позволяват да дефинираш server-side функции директно в компонентите и да ги извикваш от клиента - без ръчно създаване на API routes.
Tsx
// app/contact/page.tsx
async function submitContactForm(formData: FormData) {
  'use server' // Тази функция се изпълнява само на сървъра

  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const message = formData.get('message') as string

  await db.contactMessage.create({
    data: { name, email, message }
  })

  await sendEmailNotification({ name, email, message })
}

export default function ContactPage() {
  return (
    <form action={submitContactForm}>
      <input name="name" placeholder="Твоето име" />
      <input name="email" type="email" placeholder="Имейл" />
      <textarea name="message" placeholder="Съобщение" />
      <button type="submit">Изпрати</button>
    </form>
  )
}
Няма API route, няма fetch от клиента, няма state management за формата. Работи дори без JavaScript в браузъра - progressive enhancement по дефиниция.

Минуси на App Router - честен поглед

Би ми дошло лесно да напиша само похвали за App Router. Но реалността е по-нюансирана и справедливостта изисква да кажа и какво не работи добре - особено ако мислиш за миграция на съществуващ проект.

Стръмна крива на учене

App Router въвежда едновременно много нови концепции: Server Components, Client Components, Server Actions, Route Handlers, Parallel Routes, Intercepting Routes, новите file conventions. За разработчик, свикнал с Pages Router, това е значително когнитивно натоварване. В първите седмици на работа с App Router съм допускал грешки, които в Pages Router изобщо не биха съществували.

Проблеми с библиотеки на трети страни

Не всички библиотеки са адаптирани за Server Components. Много популярни пакети - особено UI библиотеки, state management решения и analytics инструменти - изискват клиентска среда. Преди да мигрираш, трябва да провериш всяка зависимост. Типичните проблеми: библиотека, използваща window или document на top level, Context Provider, който трябва да обвие цялото приложение, или компонент, импортиращ browser-only код без условна проверка. Решението обикновено е 'use client' wrapper или dynamic import с { ssr: false }, но това изисква допълнителна работа.

Кеширането е сложно

App Router има агресивна кеширащ стратегия с четири различни нива: Request Memoization, Data Cache, Full Route Cache и Router Cache. В Pages Router кешът беше по-прост и предвидим. В App Router е мощен, но изисква разбиране - иначе получаваш или остарели данни, или ненужни заявки.
Tsx
// Различни стратегии за кеширане в App Router
const data1 = await fetch('/api/data')                          // Кешира се неограничено
const data2 = await fetch('/api/data', { cache: 'no-store' })  // Никога не се кешира
const data3 = await fetch('/api/data', { next: { revalidate: 60 } }) // Revalidate на 60 секунди
const data4 = await fetch('/api/data', { next: { tags: ['products'] } }) // Tag-based revalidation

Debugging е по-труден

Когато нещо се счупи в App Router, намирането на причината е по-трудно. Кодът се изпълнява на две места - сървъра и клиента - и грешките понякога се появяват само на едното. Server Component грешките са видими в терминала, не в browser console-а. Това изисква промяна в debugging навиците.

Стъпки за миграция от Pages Router към App Router

Добрата новина: Next.js поддържа инкрементална миграция. pages/ и app/ директориите могат да съществуват едновременно в един проект. Можеш да мигрираш route по route, без да пипаш цялото приложение наведнъж.

Стъпка 1 - Подготовка и одит

Преди да докоснеш каквото и да е, направи одит на проекта:
  • Провери всяка зависимост в package.json за Server Components съвместимост
  • Идентифицирай компонентите, използващи useState, useEffect, browser APIs - те ще изискват 'use client'
  • Прегледай _app.tsx - всичко там трябва да се пренесе в app/layout.tsx
  • Провери дали имаш getServerSideProps, getStaticProps или getInitialProps - всяка се мигрира по различен начин

Стъпка 2 - Създай root layout

Първото нещо в app/ директорията е задължителният root layout:
Tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers' // Client Component за Context Providers
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Моят сайт',
  description: 'Описание на сайта',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="bg">
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
Tsx
'use client'
// app/providers.tsx
// Всички Context Providers, изискващи 'use client', отиват тук

import { ThemeProvider } from 'next-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider attribute="class">
        {children}
      </ThemeProvider>
    </QueryClientProvider>
  )
}

Стъпка 3 - Мигрирай data fetching

Всеки getServerSideProps се трансформира в async Server Component:
Tsx
// ПРЕДИ - pages/products/[id].tsx
export default function ProductPage({ product, reviews }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <ReviewsList reviews={reviews} />
    </div>
  )
}

export async function getServerSideProps({ params }) {
  const [product, reviews] = await Promise.all([
    db.product.findUnique({ where: { id: params.id } }),
    db.review.findMany({ where: { productId: params.id } }),
  ])
  return { props: { product, reviews } }
}
Tsx
// СЛЕД - app/products/[id]/page.tsx
import { ReviewsList } from '@/components/ReviewsList'

export default async function ProductPage({
  params,
}: {
  params: { id: string }
}) {
  const [product, reviews] = await Promise.all([
    db.product.findUnique({ where: { id: params.id } }),
    db.review.findMany({ where: { productId: params.id } }),
  ])

  return (
    <div>
      <h1>{product.name}</h1>
      <ReviewsList reviews={reviews} />
    </div>
  )
}
За getStaticProps с revalidate използвай fetch с next: { revalidate }:
Tsx
// ПРЕДИ - getStaticProps с revalidate
export async function getStaticProps() {
  const posts = await getPosts()
  return { props: { posts }, revalidate: 60 }
}

// СЛЕД - fetch с revalidate в Server Component
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }
  }).then(r => r.json())

  return <PostList posts={posts} />
}

Стъпка 4 - Мигрирай API Routes към Route Handlers

Tsx
// ПРЕДИ - pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    const products = await db.product.findMany()
    return res.status(200).json(products)
  }

  if (req.method === 'POST') {
    const product = await db.product.create({ data: req.body })
    return res.status(201).json(product)
  }
}
Tsx
// СЛЕД - app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET() {
  const products = await db.product.findMany()
  return NextResponse.json(products)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const product = await db.product.create({ data: body })
  return NextResponse.json(product, { status: 201 })
}

Стъпка 5 - Добави 'use client' там, където е необходимо

Всеки компонент, използващ hooks или browser APIs, трябва да получи 'use client'. Добрата практика е да идентифицираш тези компоненти предварително и да ги изолираш - придвижи интерактивната логика надолу в компонентното дърво, така че 'use client' границата да е колкото е възможно по-малка.

Кое да избереш - ръководство за решението

Ситуация
Препоръка
Причина
Нов проект от нулатаApp RouterПрепоръчан от Next.js, всички нови функции
Съществуващ малък проект (< 10 routes)Мигрирай изцялоМалко риск, голяма полза
Съществуващ среден проектИнкрементална миграцияМигрирай route по route
Голям enterprise проектИзчакай или мигрирай бавноВисок риск, нужно е планиране
Проект с много legacy библиотекиПровери съвместимостта първоМоже да блокира миграцията
Проект с Pages Router, работи добреНе бързайPages Router се поддържа дългосрочно

Често задавани въпроси


Миграцията от Pages Router към App Router не е спринт - тя е marathon, който изисква планиране, търпение и добро разбиране на новите концепции. Ако проектът ти работи добре с Pages Router, не мигрирай само защото "трябва". Мигрирай, когато имаш конкретна причина: нужда от Server Components, Streaming, Server Actions или nested layouts. За нови проекти обаче изборът е ясен - App Router е бъдещето на Next.js и правилният старт за всяко ново приложение днес.

Сподели статията:
В тази статия