Next.js App Router için opinionated, derinlemesine bir üretim referansı — RSC zihinsel modelleri, yönlendirme kalıpları, önbellekleme semantiği, Server Actions, Proxy ve üretim kalıpları. Next.js 16 baz alınmıştır. Tutorial değil. Geri döndüğün referans.
Bu bir "başlangıç rehberi" değil. Resmi dokümantasyon bunu zaten iyi yapıyor. Bu, Pages Router'dan App Router'a geçerken — ve Next.js 16 önbellekleme modelini tamamen değiştirdiğinde — keşke var olmasını istediğim referans.
Her bölüm tek bir soruyu yanıtlar: Bunu üretimde nasıl kullanırım?
Next.js 16 / React 19'u kapsar. Next.js 14–15 kullanıyorsan önbellekleme bölümü farklılık gösterir. Eski fetch() semantiği için Önceki Model rehberine bak.
Herhangi bir API'ye dokunmadan önce, her şeyi açıklayan tek bir zihinsel modele ihtiyacın var.
App Router, sunucu öncelikli bir framework'tür. Her bileşen varsayılan olarak Server Component'tir. İstemciye geçmeyi sen seçiyorsun.
Bu, alışkın olduğunun tam tersi. Pages Router'da (ve RSC öncesi React'ta) getServerSideProps ile açıkça sunucuda veri getirmediğin sürece her şey istemci taraflıydı. Artık sunucu varsayılan — istemci istisna.
Sunucu Dünyası İstemci Dünyası
───────────────────────────── ─────────────────────────────
Node.js / Edge üzerinde çalışır Tarayıcıda çalışır
DB, env, dosya sistemi okuyabilir window, DOM, localStorage erişebilir
useState, useEffect yok Tam React hook'ları mevcut
Kullanıcı olaylarını işleyemez onClick, onChange işleyebilir
İstek başına bir kez render olur State değişiminde yeniden render
Bu iki dünya doğrudan karışamaz. Bir Server Component, Client Component'i import edip render edebilir. Bir Client Component, Server Component'i import edemez — ama children olarak kabul edebilir (bileşim kalıbı).
Etkileşim gerekiyor mu?
├── Hayır → Server Component (varsayılan)
│ Veri getiriyor mu?
│ ├── Evet → doğrudan fetch() veya DB sorgusu
│ └── Hayır → saf UI, yine sunucu
└── Evet → useState / useEffect / olay yöneticisi gerekiyor mu?
├── Evet → 'use client'
└── Belki → önce Server Component dene,
yalnızca etkileşimli kısmı Client Component olarak ayır
Hedef, 'use client''i olabildiğince derinden itmek. Bir sayfa, tek küçük bir <BeğeniButonu> Client Component'i render eden bir Server Component olabilir. Bu ideal — "bu sayfa state kullanıyor, dolayısıyla her şey istemci" değil.
'use client' bir modül sınırı işaretçisidir, bileşen niteliği değil. Bunu bir dosyaya eklediğinde şunu söylüyorsun: "bu modül ve import ettiği her şey istemcide çalışır."
Bunun kritik çıkarımı: Bir Server Component, Client Component import eden bir modülü import ederse, tüm import zinciri istemci koduna dönüşür. Sunucuya özel kod yolları bu şekilde yanlışlıkla tarayıcıya gönderilir.
// ❌ Hata — client dosyasında sunucu-özel modül import etmek'use client'import { db } from '@/lib/database' // Node.js API'leri kullanıyor// ✅ Veri getirmeyi sunucuda tut, sonuçları prop olarak geçimport { db } from '@/lib/database'import { ClientWidget } from './ClientWidget'export default async function Page() { const data = await db.query(...) return <ClientWidget initialData={data} />}
Bunu derleme zamanında zorlamak için server-only kullan:
Kullanıcı A → B → A arası geziniyor
Layout: [A bir kez render olur, B boyunca kalır, A'ya döner — yeniden mount yok]
Template: [A mount olur] → [A unmount, B mount] → [B unmount, A taze mount]
Layout kullan: navigasyon çubukları, kenar çubukları, rotalar arası state koruması gereken her şey.
Template kullan: sayfa geçiş animasyonları, her navigasyonda tetiklenmesi gereken analitik olaylar, bileşen state'ini kasıtlı sıfırlama.
// app/layout.tsx — kalıcı, state navigasyonlarda hayatta kalırexport default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <Kenar /> {/* bir kez render olur */} {children} </body> </html> )}// app/template.tsx — her navigasyonda yeniden oluşturulur'use client'import { motion } from 'framer-motion'export default function Template({ children }: { children: React.ReactNode }) { return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}> {children} </motion.div> )}
app/
├── blog/[slug]/page.tsx → /blog/herhangi-bir-şey
├── magaza/[...kategoriler]/page.tsx → /magaza/a/b/c (bir veya daha fazla)
└── docs/[[...yol]]/page.tsx → /docs VE /docs/a/b/c (sıfır veya daha fazla)
Next.js 15+'ta params ve searchParamsPromise'tir. Her zaman await et:
Yol kesen rotalar, mevcut layout içinde bir rota yüklemenizi sağlar. Tipik örnek: beslemeyi görünür tutarken fotoğrafı modal'da açmak, ama fotoğraf doğrudan URL ile paylaşılabilir.
Cache Components etkinken Next.js 16, Kısmi Ön İşlemeyi (PPR) varsayılan olarak kullanır. Her rota derleme zamanında statik bir kabuk üretir; dinamik delikler Suspense sınırları aracılığıyla istek zamanında akıtılır.
Statik kabuk (derleme zamanında üretilir, edge'den sunulur)
└── <Suspense> sınırları = "dinamik delikler"
└── İstek başına sunucudan akıtılır
Derleme sırasında her bileşenin nasıl ele alındığı:
Bileşen ne yapıyor
Derleme davranışı
'use cache' direktifi
Önbelleğe alınır, statik kabukta yer alır
Saf hesaplamalar, modül import'ları
Otomatik olarak statik kabukta
Dinamik içeriği saran <Suspense>
Statik kabukta fallback, içerik istek zamanında akıtılır
// next.config.tsimport type { NextConfig } from 'next'const nextConfig: NextConfig = { cacheComponents: true,}export default nextConfig
Bu etkinleştirildiğinde, <Suspense> sınırı dışında önbelleğe alınmamış veriye erişirsen Next.js derleme hatası fırlatır: Uncached data was accessed outside of <Suspense>.
// Rota segment config (page.tsx veya layout.tsx)export const dynamic = 'force-dynamic' // her zaman istek başına server-renderexport const dynamic = 'force-static' // her zaman statik, dinamik API kullanılırsa hataexport const revalidate = 60 // ISR — 60 saniyede bir yenileexport const revalidate = false // tam statik, asla yenileme
İstek zamanı verisi gerektiğinde ama bunu açık belirtmek istediğinde:
import { connection } from 'next/server'export default async function Page() { await connection() // açıkça dinamik render'a geç const id = crypto.randomUUID() // güvenli — istek zamanında çalışır return <div>İstek ID: {id}</div>}
Streaming, UI'yi aşamalı olarak render etmeni sağlar. Statik kabuğu hemen gönder, dinamik parçaları hazır oldukça akıt.
// Streaming olmadan — TÜM veri getirilene kadar hiçbir şey görünmezexport default async function Panel() { const [kullanici, analitik, aktivite] = await Promise.all([ getKullanici(), getAnalitik(), // yavaş — 2s getAktivite(), // yavaş — 1.5s ]) return <PanelUI kullanici={kullanici} analitik={analitik} aktivite={aktivite} />}// Streaming ile — layout hemen görünür, veri akıtılırimport { Suspense } from 'react'export default async function Panel() { const kullanici = await getKullanici() // hızlı — layout için gerekli return ( <PanelLayout kullanici={kullanici}> <Suspense fallback={<AnalitikIskeleti />}> <Analitik /> {/* kendi verisini getirir, hazır olunca akıtılır */} </Suspense> <Suspense fallback={<AktiviteIskeleti />}> <Aktivite /> {/* bağımsız — Analitik'i bloklamaz */} </Suspense> </PanelLayout> )}async function Analitik() { const data = await getAnalitik() // 2s — Aktivite'yi bloklamaz return <AnalitikGrafik data={data} />}
loading.tsx, page.tsx etrafındaki Suspense sınırı için sözdizimsel şekerdir. Rota seviyesi yükleme durumları için loading.tsx kullan. Bileşen seviyesi ayrıntısı için satır içi <Suspense> kullan.
Bu, App Router'ın en karmaşık kısmı — ve Next.js 16'da model önemli ölçüde değişti. Eski fetch() önbellekleme semantiği artık "Önceki Model" olarak adlandırılıyor. Yeni model use cache direktifini kullanıyor.
Stale-while-revalidate — bayat hemen sun, taze arka planda
Önbelleği hemen sona erdirir — kullanıcı bir sonraki istekte taze görür
Kullanım
Blog yazıları, ürün katalogları — hafif gecikme kabul edilebilir
Kullanıcının kendi verisi — read-your-own-writes
import { revalidateTag, updateTag } from 'next/cache'// Admin bir blog yazısı yayınladıktan sonra — arka plan yenileme uygundurexport async function yaziYayinla(id: string) { 'use server' await db.yazilar.yayinla(id) revalidateTag('yazilar', 'max') // 'max' = bayat pencere süresi}// Kullanıcı profilini güncelledikten sonra — değişikliğini hemen görmelidirexport async function profilGuncelle(data: ProfilVerisi) { 'use server' await db.profiller.guncelle(data) updateTag('profil') // hemen sona erdirir, sonraki istek taze olur redirect('/profil')}
Server Actions, istemciden çağrılan, sunucuda çalışan async fonksiyonlardır. Mutasyonlar için API rotalarının yerini alır.
Güvenlik: Server Actions doğrudan POST istekleriyle erişilebilir — yalnızca UI'dan değil. Her action'da her zaman kimlik doğrulama ve yetkilendirme doğrula.
'use server'export async function yorumSil(yorumId: string) { // ❌ Yalnızca kimlik doğrulamayı kontrol eder, sahipliği değil const session = await auth() if (!session) throw new Error('Yetkisiz') await db.yorumlar.delete(yorumId) // herkes herkesin yorumunu silebilir!}export async function yorumSil(yorumId: string) { // ✅ Sahipliği de kontrol eder const session = await auth() if (!session) throw new Error('Yetkisiz') const yorum = await db.yorumlar.findById(yorumId) if (!yorum) throw new Error('Bulunamadı') if (yorum.yazarId !== session.user.id) throw new Error('Yasak') await db.yorumlar.delete(yorumId) revalidatePath('/yorumlar')}
Next.js 16'da middleware.ts, amacını daha iyi yansıtmak için proxy.ts olarak yeniden adlandırıldı. İşlevsellik aynı — Edge Runtime'da her istekten önce çalışır. Export ismi proxy (ya da default export).
Not: Üçüncü taraf kütüphaneler buna hâlâ middleware diyebilir. Next.js 16 itibarıyla dahili olarak proxy.
// proxy.ts — proje kökünde (veya /src) olmak zorundaimport { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'export function proxy(request: NextRequest) { return NextResponse.next()}// Alternatif olarak, default exportexport default function proxy(request: NextRequest) { return NextResponse.next()}// Yalnızca belirli yollarda çalıştırexport const config = { matcher: [ '/panel/:path*', '/api/:path*', '/((?!_next/static|_next/image|favicon.ico).*)', ],}
En yaygın kullanım durumu — kimlik doğrulanmamış kullanıcıları yönlendir. Proxy yalnızca iyimser kontroller için kullanılır. Burada veritabanı araması yapma — her istek için çalışır, prefetch'ler dahil.
// proxy.tsimport { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'import { decrypt } from '@/lib/session'const korunanRotalar = ['/panel', '/ayarlar', '/api/korunan']const acikRotalar = ['/giris', '/kayit', '/']export async function proxy(req: NextRequest) { const yol = req.nextUrl.pathname const korunanRota = korunanRotalar.some(r => yol.startsWith(r)) const acikRota = acikRotalar.includes(yol) // Çerezden oturumu oku — iyimser kontrol, DB araması yok const cerez = req.cookies.get('session')?.value const session = await decrypt(cerez) // yalnızca JWT doğrulaması if (korunanRota && !session?.userId) { const girisUrl = new URL('/giris', req.url) girisUrl.searchParams.set('callbackUrl', yol) return NextResponse.redirect(girisUrl) } if (acikRota && session?.userId && !yol.startsWith('/panel')) { return NextResponse.redirect(new URL('/panel', req.url)) } // Kullanıcı bilgisini başlıklara ekle const yanit = NextResponse.next() if (session?.userId) { yanit.headers.set('x-kullanici-id', session.userId) yanit.headers.set('x-kullanici-rolu', session.role ?? '') } return yanit}export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$).*)'],}
Server Component'te eklenen başlıkları okuma:
// app/panel/page.tsximport { headers } from 'next/headers'export default async function PanelSayfasi() { const basliklar = await headers() const kullaniciId = basliklar.get('x-kullanici-id') // Token'ı tekrar doğrulamaya gerek yok — Proxy zaten yaptı const kullanici = await getKullaniciById(kullaniciId!) return <Panel kullanici={kullanici} />}
Auth için Node.js API'lerine ihtiyacın varsa, Proxy'de yalnızca JWT imzasını doğrula (hafif), tam oturum aramasını Server Component veya Route Handler'da yap.
Next.js 16, bir rotaya gezinim yapmanın anında olduğunu (Suspense sınırları dışında önbelleğe alınmamış veri bloke etmediğini) doğrulamak için unstable_instant sunar:
Performans
☐ Tüm görseller için next/image (width, height, alt, katlamalar için priority)
☐ Tüm özel fontlar için next/font
☐ 50KB üzeri bileşenler için dinamik import'lar
☐ Bundle analiz edildi — client bundle'da sürpriz büyük paket yok
☐ Lighthouse'da Core Web Vitals yeşil
Güvenlik
☐ next.config.ts'te güvenlik başlıkları yapılandırıldı
☐ Tüm Server Actions kimlik doğrular + yetkilendirir (sadece giriş kontrolü değil)
☐ Kaynak sahipliği kontrol edildi (kullanıcı mutasyonu yaptığı şeye sahip mi)
☐ NEXT_PUBLIC_ env değişkenlerinde sır yok
☐ Auth uç noktaları ve maliyetli işlemlerde hız sınırlama
☐ CSP yapılandırıldı (en azından default-src 'self')
Önbellekleme
☐ Kullanıcıya özel veri sessionId olmadan 'use cache'de değil
☐ Mutasyonlar revalidatePath / revalidateTag / updateTag çağırıyor
☐ Runtime API'leri (cookies, headers) Suspense sınırları içinde
☐ cacheLife profilleri içerik tazelik gereksinimlerine uygun
SEO
☐ Tüm genel sayfalarda generateMetadata var
☐ OG görselleri yapılandırıldı (1200x630)
☐ sitemap.ts / robots.ts mevcut
☐ Kanonik URL'ler ayarlandı
Güvenilirlik
☐ Tüm kritik segmentlerde error.tsx var
☐ Kökte not-found.tsx var
☐ Tüm async içerik için yükleme durumları var
☐ Ortam değişkenleri başlangıçta doğrulandı (hızlı başarısız ol)
☐ Kritik navigasyon rotalarında unstable_instant
app/
├── layout.tsx Kalıcı sarmalayıcı (navda yeniden mount yok)
├── template.tsx Taze sarmalayıcı (navda yeniden mount)
├── page.tsx Rota UI'ı
├── loading.tsx Sayfa için Suspense fallback'i
├── error.tsx Hata sınırı ('use client' gerekli)
├── not-found.tsx 404 UI'ı
├── route.ts API yöneticisi (GET, POST, vb.)
├── proxy.ts Edge proxy (kök veya /src) — eskiden middleware.ts'ti
└── [klasor]/
├── (grup)/ Rota grubu — URL segmenti yok
├── [param]/ Dinamik segment
├── [...geri]/ Hepsini yakala (bir veya daha fazla)
├── [[...geri]]/ İsteğe bağlı hepsini yakala (sıfır veya daha fazla)
├── @slot/ Paralel rota slotu
└── (.)kes/ Yol kesen rota
Veri kullanıcıya özel mi veya cookies/headers gerektiriyor mu?
├── Evet → Runtime verisi — <Suspense> içinde, 'use cache' yok
└── Hayır → Bir süre bayat olabilir mi?
├── Hayır → <Suspense> içinde, 'use cache' yok
└── Evet → 'use cache' + cacheLife(...)
└── Mutasyonlara göre değişiyor mu?
├── Evet → cacheTag(...) + mutasyonda revalidateTag/updateTag
└── Hayır → cacheLife('max') veya 'weeks'
Son güncelleme: Nisan 2026. Next.js 16.x / React 19'u kapsar.