Pages Router'dan App Router'a Migration Rehberi: Next.js 16 Adım Adım Geçiş (2026)

Next.js 16'da Pages Router'dan App Router'a kademeli geçiş için pratik bir rehber. getServerSideProps, _app.tsx, pages/api ve next/router dönüşümlerini gerçek kod örnekleriyle adım adım gösteriyoruz.

Next.js 16 App Router Geçiş Rehberi (2026)

Güncellendi: 4 Eylül 2026

Pages Router'dan App Router'a geçiş, mevcut pages/ dizinini olduğu gibi bırakıp yanına app/ dizinini ekleyerek başlar; Next.js 16, iki router'ı aynı projede yan yana çalıştırdığı için tüm uygulamayı bir gecede değil, rota rota taşıyabilirsiniz. Yani migration aslında "büyük bir rewrite" değil; kademeli, geri alınabilir bir mimari değişiklik. Bu rehberde getServerSideProps'tan Server Components'a, _app.tsx'ten root layout'a, pages/api'den Route Handlers'a kadar tüm dönüşümleri gerçek kod örnekleriyle göstereceğim. Uzun yıllardır Next.js kullandığım için (ve son iki büyük projemi tam olarak bu yolla taşıdığım için) Pages tarafındaki alışkanlıkların App Router ile nerede çakıştığını da açıkça belirteceğim. Dürüst olmam gerekirse, ilk denememde params'ı await etmeyi unuttuğum için yarım saat "neden undefined?" diye baktım; siz aynı tuzağa düşmeyin.

  • Next.js 16'da pages/ ve app/ dizinleri aynı projede yan yana çalışabilir, bu sayede migration kademeli yapılabilir.
  • getServerSideProps yerine async Server Components ve fetch() ile cache: 'no-store' kullanılır; getStaticProps yerine varsayılan static rendering geçerlidir.
  • _app.tsx ve _document.tsx tek bir app/layout.tsx root layout'a dönüşür; layout'lar iç içe konumlanır ve yeniden mount olmaz.
  • pages/api/* handler'ları Route Handlers'a (app/api/*/route.ts) taşınır; imza Request / Response Web API'lerine dayanır.
  • next/router paketi App Router'da çalışmaz; next/navigation'a geçiş şarttır ve router.push, usePathname, useSearchParams davranışı değişir.
  • Metadata, veri çekimi, error/loading state'leri ve middleware davranışında ince farklar vardır; en sık migration bug'ları burada çıkar.

Pages Router ve App Router arasındaki temel farklar

App Router, üç yapısal değişiklikle Pages Router'dan ayrılıyor: dosya-bazlı layout mimarisi, React Server Components (RSC) ve segment-bazlı caching. Pages tarafında her sayfa bir React ağacı köküydü; _app.tsx ile sardığınız her şey rota değişince yeniden mount olurdu. App Router'da layout.tsx dosyaları rota segmentleri arasında paylaşılır ve yeniden render edilmez. Bu, sidebar, tema state'i ve auth context gibi bileşenlerin sekmeler arasında canlı kalmasını sağlar.

Server Components ise Pages Router'daki tek "server" kavramı olan getServerSideProps'un yerine geçmez; onu tamamen ortadan kaldırır. Bir bileşenin kendisi sunucuda çalışır, DB'den veri çeker, HTML üretir ve tarayıcıya JavaScript göndermeden ekrana yazılır. Bu yüzden useState, useEffect gibi client hook'ları Server Component'ta çalışmaz; interaktif kısımlar için "use client" direktifiyle Client Component'lara bölünürsünüz.

Üçüncü fark caching: Pages'ta getStaticProps ve ISR ile sayfa seviyesinde cache verirdiniz. App Router'da fetch() çağrıları segment bazında cache'lenir, use cache direktifi ile fonksiyon seviyesinde önbellek tanımlanır ve Cache Components ile ince taneli invalidation yapılır. Detaylı invalidation stratejileri için Next.js App Router önbellekleme rehberimize göz atabilirsiniz.

Migration'a başlamadan önce hazırlık adımları

Kodu tek bir feature branch'te tutmak yerine migration'ı iki aşamalı bir sürece bölmenizi tavsiye ederim: önce Next.js 16'ya güvenli yükseltme (hâlâ Pages Router'da), sonra rota rota App Router taşıması. Bu, "hangi bug yükseltmeden, hangisi mimari değişiklikten geldi?" sorusunu ortadan kaldırır.

  1. TypeScript sürümünüzü kontrol edin. App Router async component tiplerinin doğru çalışması için TypeScript 5.4+ önerilir.
  2. React 19'a geçin. Next.js 16 varsayılan olarak React 19 ile gelir; useOptimistic ve useFormStatus gibi hook'ları migration sonrasında Server Actions ile birlikte kullanacaksınız.
  3. Test coverage ekleyin. En kritik user flow'lar için Playwright end-to-end testleri yazın. RSC'ye geçince "aynı ekrana çıkıyor gibi görünüyor ama form submit çalışmıyor" senaryoları yakalanmalı.
  4. Third-party bağımlılıkları denetleyin. Client-only olan analytics, chart ve datepicker kütüphaneleri Server Component'ta hata verir; her birinin "use client" wrapper'ı gerektirir.
  5. Bir "kanarya" rotası seçin. Genellikle /about veya /blog gibi düşük riskli sayfa, ilk App Router deneyimi için ideal.

Next.js 16'ya yükseltme ve interoperability

Kritik olan şu: Next.js 16 hem pages/ hem app/ dizinini destekliyor. Bir sayfa aynı yolda hem pages/blog.tsx hem app/blog/page.tsx olarak var olamaz; App Router öncelikli çalışır ve build sırasında uyarı görürsünüz. Ama /blog'u App Router'a taşırken /dashboard'u pages/ içinde bırakabilirsiniz.

// package.json güncellemesi
{
  "dependencies": {
    "next": "^16.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Yükseltmeden sonra next.config.ts'de eski Pages özelliklerini bozmayacak minimal bir yapılandırma bırakın:

// next.config.ts
import type { NextConfig } from 'next';

const config: NextConfig = {
  reactStrictMode: true,
  // App Router aktifken de pages dizini otomatik çalışmaya devam eder
  experimental: {
    // Kademeli migration için PPR'ı önce sadece App Router rotalarında deneyin
    ppr: 'incremental',
  },
};

export default config;

Codemod'lar migration'ın hız çarpanı: npx @next/codemod@latest next-async-request-api . komutu cookies(), headers() ve params çağrılarını Next.js 16'nın async imzasına dönüştürür. Ben genellikle codemod'u önce çalıştırıp sonra diff'i satır satır inceliyorum, çünkü otomatik dönüşüm her zaman doğru olmuyor.

app/ dizini ve root layout oluşturma

App Router'ın ilk zorunlu dosyası app/layout.tsx ve içinde <html> ile <body> etiketlerini siz render etmek zorundasınız. Pages Router'da bunlar _document.tsx ile yönetiliyordu; artık root layout ikisinin de yerine geçiyor.

// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

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

export const metadata: Metadata = {
  title: {
    default: 'Uygulamam',
    template: '%s | Uygulamam',
  },
  description: 'Next.js 16 App Router uygulaması',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="tr">
      <body className={inter.className}>
        {/* _app.tsx'teki global provider'lar buraya taşınır */}
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

_app.tsx'te sarmalanan Context Provider'ları (Theme, Auth, ReactQuery) Client Component olmak zorunda çünkü useState ve useContext kullanıyorlar. Onları ayrı bir dosyaya çıkarın:

// app/providers.tsx
'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>
  );
}

Bu yapıda root layout Server Component olarak kalır, provider'lar Client Component sınırında başlar. Bu ayrım performansı doğrudan etkiliyor: root layout'u client'a çekerseniz tüm sayfa ağacı hidrasyona zorlanır.

getServerSideProps ve getStaticProps'u Server Components'a çevirmek

Bu, migration'ın en büyük mental değişimi. Pages tarafında veri çekimi sayfa dosyasının en üstündeki bir named export'tu; App Router'da veri çekimi bileşenin kendisidir.

// ÖNCE: pages/blog/[slug].tsx (Pages Router)
export async function getServerSideProps({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then((r) => r.json());
  return { props: { post } };
}

export default function BlogPost({ post }) {
  return <article><h1>{post.title}</h1></article>;
}
// SONRA: app/blog/[slug]/page.tsx (App Router)
type Props = { params: Promise<{ slug: string }> };

export default async function BlogPost({ params }: Props) {
  const { slug } = await params; // Next.js 16'da params async'tir
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'no-store', // getServerSideProps ile birebir eşleşen davranış
  });
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

getStaticProps için ise varsayılan davranış zaten static rendering. Bir fetch çağrısına { next: { revalidate: 3600 } } ekleyerek ISR davranışını taklit edersiniz:

// app/pricing/page.tsx (getStaticProps + revalidate karşılığı)
export default async function PricingPage() {
  const prices = await fetch('https://api.example.com/prices', {
    next: { revalidate: 3600 }, // saatte bir invalidate ol
  }).then((r) => r.json());

  return <PriceTable data={prices} />;
}

getStaticPaths'in karşılığı generateStaticParams. İkisi de doğrudan sayfa dosyasında export edilir ve mental model aynı:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
    .then((r) => r.json());

  return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}

pages/api'yi Route Handlers'a taşıma

pages/api/*.ts handler'ları Next.js'in kendi NextApiRequest ve NextApiResponse tiplerini kullanıyordu. App Router bunu Web platform standardına (Request ve Response) çevirdi. Route Handlers app/api/[route]/route.ts dosyasında tanımlanır ve HTTP metotları için ayrı named export'lar kullanır.

// ÖNCE: pages/api/subscribe.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { email } = req.body;
  await addToNewsletter(email);
  res.status(200).json({ success: true });
}
// SONRA: app/api/subscribe/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { email } = await request.json();
  await addToNewsletter(email);
  return NextResponse.json({ success: true });
}

Method allowlist'ini artık siz yönetmiyorsunuz. Sadece POST'u export ettiyseniz Next.js diğer metotlara otomatik 405 döner. req.body'nin body-parser tarafından JSON'a çevrildiği Pages davranışı yok; kendiniz await request.json() yapıyorsunuz. Dosya yüklemeleri için await request.formData(), URL parametreleri için new URL(request.url).searchParams kullanılır.

Route Handlers'ın webhook, SSE ve streaming senaryolarındaki ileri düzey kullanımı için Route Handlers rehberimizi mutlaka okuyun.

App Router'a taşıdığınız her Client Component'ta useRouter, useSearchParams, usePathname import'larının kaynağı değişmek zorunda:

// ÖNCE
import { useRouter } from 'next/router';

// SONRA
import { useRouter, usePathname, useSearchParams } from 'next/navigation';

API'ler de tamamen aynı değil. router.query ortadan kalktı; parametreleri useSearchParams() ile alıyorsunuz. router.push('/dashboard?tab=billing') hâlâ çalışıyor ama { pathname, query } nesnesi kabul etmiyor artık; sadece string. Ayrıca router.events yok; rota değişikliği event'lerini dinlemek için usePathname ve useSearchParams'ı useEffect'in dependency array'ine koyup değişimleri gözlemliyorsunuz.

'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export function Analytics() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = `${pathname}?${searchParams.toString()}`;
    // pageview event gönder
    window.gtag?.('event', 'page_view', { page_path: url });
  }, [pathname, searchParams]);

  return null;
}

Resmi useRouter dokümantasyonunda tüm metotların yeni imzaları listelenmiş; migration sırasında yanınızda açık tutmanızı tavsiye ederim.

Head bileşeninden Metadata API'ye

Pages Router'da <Head> bileşeni sayfa gövdesine gömülüyordu; App Router'da metadata bir React ağacı değil, bir data yapısı. Sayfa ya da layout dosyasında metadata named export'u yazıyorsunuz:

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`)
    .then((r) => r.json());

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage, width: 1200, height: 630 }],
    },
  };
}

Bu yapıyla <Head> importuna hiç ihtiyaç kalmıyor. Dinamik Open Graph görselleri, sitemap ve robots.txt üretimi için ise ayrı file convention'lar var: opengraph-image.tsx, sitemap.ts, robots.ts. Detaylı SEO stratejisini Next.js 16 Metadata API rehberimizde anlattık.

Sık karşılaşılan migration hataları

Kendi projelerimde ve code review'larda tekrar tekrar gördüğüm hatalar:

1. Server Component içinde client hook'ları kullanmak

"You're importing a component that needs useState" hatası alıyorsanız o bileşenin dosyasına 'use client' direktifi ekleyin. Ancak tüm ağacı client'a taşımayın; sadece hook'a ihtiyaç duyan yaprağı.

2. window veya document'a doğrudan erişim

Server Component'lar server'da render olur, window yoktur. Analitik snippet gibi client-only kodlar useEffect içinde yürütülmeli. Şart değilse next/dynamic ile { ssr: false } seçeneği uygulanabilir.

3. Import path aynı ama davranış farklı

import { redirect } from 'next/navigation' Server Component'ta throw eder, o yüzden try/catch içine almayın; aksi halde redirect yakalanır. Ayrıca notFound() da aynı mekanizmayla çalışıyor.

4. Cache invalidation'ı yanlış hedeflemek

revalidatePath'in dinamik segmentlerde çalışması için "layout" veya "page" tipini belirtmek gerekiyor. revalidatePath('/blog/[slug]', 'page') yazmak, revalidatePath('/blog/hello-world')'ten farklı bir invalidasyon yapıyor. Yanlış hedefleme, "değişiklik canlıya çıkmadı" sorunlarının en yaygın nedeni.

5. Client Component'a Server Component prop olarak geçirmek

"props must be serializable" hatası: Client Component'a fonksiyon prop geçemezsiniz. Server Component'ı children olarak Client Component'a geçirebilirsiniz. Bu pattern, tema wrapper'ları için sıkça kullanılır.

Turbopack ve build zamanları

App Router'a taşındıktan sonra dev server ve build sürelerinde en somut hızlanmayı Turbopack sağlıyor. Next.js 16'da next dev --turbo stabil ve varsayılan; production build'lerde Turbopack'i etkinleştirmek için next build --turbo flag'i kullanılıyor. Ben orta ölçekli bir e-ticaret projesinde build süresinin 3 dakika 40 saniyeden 1 dakika 05 saniyeye düştüğünü gördüm. Yapılandırma detayları için Next.js 16 Turbopack rehberimize göz atın.

Sık sorulan sorular

Pages Router hâlâ destekleniyor mu?

Evet, Next.js 16'da Pages Router hâlâ tam destekli ve deprecate edilmedi. Vercel, Pages Router için uzun vadeli destek sözü verdi; App Router'a geçmek zorunda değilsiniz, ancak yeni özelliklerin çoğu (Server Actions, PPR, streaming metadata) sadece App Router'da kullanılabiliyor.

Tüm sayfaları aynı anda mı taşımam gerekiyor?

Hayır. pages/ ve app/ dizinleri aynı projede yan yana çalışabilir. Genellikle önce marketing sayfalarını (about, pricing, blog) taşımak, ardından dashboard'lara geçmek en düşük riskli yaklaşımdır. Rota bazlı migration sayesinde canlı trafik akarken parça parça göç yapabilirsiniz.

getInitialProps hâlâ çalışıyor mu?

getInitialProps Pages Router içinde çalışmaya devam ediyor ama App Router'da desteklenmiyor. App Router'a taşıdığınız her sayfada onu Server Component'taki async veri çekim pattern'ine dönüştürmeniz gerekiyor. Ben genellikle önce getServerSideProps'a çevirip test ediyorum, sonra App Router'a taşıyorum; iki adımlı yaklaşım hata ayıklamayı bayağı kolaylaştırıyor.

next/head bileşeni App Router'da çalışır mı?

Hayır. App Router'da next/head'in yerini Metadata API alıyor. Sayfa veya layout dosyasında metadata objesini export ediyor ya da generateMetadata fonksiyonu yazıyorsunuz. Migration sırasında IDE'niz next/head import'unu görürse mutlaka refactor edin; build hata vermez ama tag'ler sessizce etkisiz kalır.

Auth logic'imi nereye taşımalıyım?

Pages Router'da _app.tsx içinde tuttuğunuz oturum kontrolünü App Router'da Server Component olan layout.tsx'e veya Route Handler'lara taşıyabilirsiniz. Middleware'de ilk seviye erişim kontrolü yapıp ayrıntılı yetkilendirmeyi layout'ta yürütmek en güvenli yaklaşımdır. Auth.js v5 ile RBAC pattern'lerini Auth.js kimlik doğrulama rehberimizde anlattık.

Ben Howard
Yazar Hakkında Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.