yigityalim/x/github/işe al/paylaş
El Kitaplarına Dön

Next.js App Router El Kitabı

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.

Son güncelleme: 2026-04-16

Teknoloji Yığını

Next.jsReactTypeScriptTailwind CSSSupabaseVerceltRPC

Bağlantılar

GitHub
ÖncekiTurborepo ile Monorepo MimarisiSonrakiKriptografi El Kitabı
© 2026 Yiğit Yalım. Tüm hakları saklıdır.
/

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.


İçindekiler

  1. Zihinsel Model
  2. Yönlendirme
  3. Render
  4. Veri Getirme
  5. Önbellekleme ve Yeniden Doğrulama
  6. Server Actions
  7. Proxy
  8. Kalıplar
  9. Üretim Kontrol Listesi

1. Zihinsel Model

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.

İki Dünya

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ı).

Karar Ağacı

Yeni bir bileşen oluştururken sor:

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" Gerçekte Ne Anlama Gelir

'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:

// lib/database.ts
import 'server-only' // Client bundle'da import edilirse derleme hatası
export const db = ...

Next.js, server-only import'larını dahili olarak yönetir — kısıtlama zorlanır.


2. Yönlendirme

Dosya Kuralları

App Router, klasörlerin rotaları, dosyaların UI'yi tanımladığı dosya sistemi kuralı kullanır. Temel dosyalar:

DosyaAmaç
page.tsxRota segmenti için özgün UI. Rotayı herkese açık hale getirir.
layout.tsxAlt segmentleri saran paylaşılan UI. Navigasyonlarda kalıcıdır — yeniden mount olmaz.
template.tsxLayout gibi ama her navigasyonda yeni örnek oluşturur. Animasyonlar için.
loading.tsxOtomatik Suspense sınırı. Sayfa yüklenirken gösterilir.
error.tsxSegment için hata sınırı. 'use client' olmak zorunda.
not-found.tsxSegment içinden notFound() çağrıldığında render edilir.
route.tsAPI uç noktası. UI yok.
proxy.tsHer istekten önce çalışan Edge fonksiyonu (Next.js 16 öncesi middleware.ts'ti).

Layout vs Template

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ır
export 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>
  )
}

Dinamik Rotalar

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 searchParams Promise'tir. Her zaman await et:

// app/blog/[slug]/page.tsx
export default async function BlogYazisi({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ sayfa?: string }>
}) {
  const { slug } = await params
  const { sayfa } = await searchParams
 
  const yazi = await getYazi(slug)
  if (!yazi) notFound()
 
  return <Makale yazi={yazi} />
}
 
// Derleme zamanında statik yollar oluştur
export async function generateStaticParams() {
  const yazilar = await getTumYazilar()
  return yazilar.map((yazi) => ({ slug: yazi.slug }))
}

Ya da next dev / next build ile üretilen global PageProps helper'ını kullan:

export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
  return <h1>Blog yazısı: {slug}</h1>
}

Rota Grupları

Rota grupları (isim), URL'yi etkilemeden rotaları düzenler.

Birden fazla kök layout:

app/
├── (pazarlama)/
│   ├── layout.tsx     → açılış sayfaları layout'u
│   ├── page.tsx       → /
│   └── hakkinda/page.tsx → /hakkinda
└── (uygulama)/
    ├── layout.tsx     → kimlik doğrulanmış uygulama layout'u
    ├── panel/page.tsx → /panel
    └── ayarlar/page.tsx → /ayarlar

URL segmenti olmadan paylaşılan layout:

app/
└── (auth)/
    ├── layout.tsx       → giris ve kayit tarafından paylaşılan
    ├── giris/page.tsx   → /giris
    └── kayit/page.tsx   → /kayit

Paralel Rotalar

Paralel rotalar, aynı layout'ta birden fazla sayfayı eş zamanlı render eder. Her slotun kendi loading.tsx ve error.tsx'i vardır.

app/
└── panel/
    ├── layout.tsx
    ├── page.tsx
    ├── @analitik/
    │   ├── page.tsx
    │   └── loading.tsx
    └── @aktivite/
        ├── page.tsx
        └── loading.tsx
// app/panel/layout.tsx
export default function PanelLayout({
  children,
  analitik,
  aktivite,
}: {
  children: React.ReactNode
  analitik: React.ReactNode
  aktivite: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-12">
      <main className="col-span-8">{children}</main>
      <aside className="col-span-4">
        {analitik} {/* bağımsız yüklenir */}
        {aktivite}  {/* bağımsız yüklenir */}
      </aside>
    </div>
  )
}

Slotlara her zaman default.tsx ekle — eşleşen sayfası olmayan slotlar için gerekli:

// app/panel/@analitik/default.tsx
export default function Default() {
  return null // veya iskelet
}

Yol Kesen Rotalar

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.

app/
├── besleme/page.tsx
└── fotograf/
    ├── [id]/page.tsx              → /fotograf/123 (tam sayfa — doğrudan URL)
    └── (..)fotograf/[id]/page.tsx → besleme'den gezinim yaparken keser

Kural:

  • (.) — aynı seviye
  • (..) — bir üst seviye
  • (..)(..) — iki seviye yukarı
  • (...) — kökten

Instagram tarzı fotoğraf modal'ı için paralel rotalarla birleştir.


3. Render

Render Modeli

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ıyorDerleme 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
cookies(), headers(), searchParamsDinamik — <Suspense> içinde olmak zorunda
Önbelleğe alınmamış fetch() veya DB sorgularıDinamik — <Suspense> içinde olmak zorunda

Cache Components'i Etkinleştirme

// next.config.ts
import 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>.

Dinamik Render'ı Zorlamak

Rotanın her zaman dinamik olmasını istediğinde:

// Rota segment config (page.tsx veya layout.tsx)
export const dynamic = 'force-dynamic' // her zaman istek başına server-render
export const dynamic = 'force-static'  // her zaman statik, dinamik API kullanılırsa hata
export const revalidate = 60           // ISR — 60 saniyede bir yenile
export const revalidate = false        // tam statik, asla yenileme

Dinamik Render'a Açıkça Geçiş

İ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

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ünmez
export 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ır
import { 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.


4. Veri Getirme

Server Component'lerde Veri Getirme

Async Server Component'lerde doğrudan veri getir — veritabanından, ORM'den veya HTTP API'den:

// fetch ile
export default async function Page() {
  const res = await fetch('https://api.ornek.com/yazilar')
  const yazilar = await res.json()
  return <YaziListesi yazilar={yazilar} />
}
 
// ORM ile — mükemmel güvenli, yalnızca sunucu tarafında çalışır
import { db } from '@/lib/db'
 
export default async function Page() {
  const yazilar = await db.select().from(yazilarTablosu)
  return <YaziListesi yazilar={yazilar} />
}

Paralel vs Sıralı Getirme

// ❌ Sıralı — 3 tur gidiş-dönüş
async function Page() {
  const kullanici = await getKullanici()             // 100ms
  const yazilar = await getYazilar(kullanici.id)     // 200ms — kullanıcıyı bekler
  const yorumlar = await getYorumlar()               // 150ms — yazıları bekler
  // Toplam: ~450ms
}
 
// ✅ Paralel — aynı anda başlar
async function Page() {
  const [kullanici, yazilar, yorumlar] = await Promise.all([
    getKullanici(),
    getYazilar(),
    getYorumlar(),
  ])
  // Toplam: ~200ms (en yavaş kazanır)
}
 
// ✅ Veri önceki veriye bağlıyken sıralı doğrudur
async function Page({ params }: { params: Promise<{ kullaniciId: string }> }) {
  const { kullaniciId } = await params
  const kullanici = await getKullanici(kullaniciId)
  if (!kullanici) notFound()
 
  const yazilar = await getKullanicininYazilari(kullanici.id) // doğru — bağımlı
  return <KullaniciProfili kullanici={kullanici} yazilar={yazilar} />
}

Tekilleştirme için React cache()

cache(), fonksiyonu istek başına memoize eder. Aynı render'da getKullanici('123') çağıran iki bileşen veritabanına yalnızca bir kez gider:

import { cache } from 'react'
 
export const getKullanici = cache(async (id: string) => {
  return db.kullanicilar.findById(id)
})
 
// layout.tsx içinde — DB'ye gider
const kullanici = await getKullanici(params.id)
 
// page.tsx içinde (aynı istek) — önbelleğe alınan sonucu döner, DB çağrısı yok
const kullanici = await getKullanici(params.id)

App Router'da context tabanlı veri geçişinin yerini alır. Hem layout hem page aynı önbelleğe alınmış fonksiyonu çağırır — prop drilling yok.

use API ile Veri Akıtma

Server Component'ten Client Component'e Promise geçir, use() ile çöz:

// app/blog/page.tsx — Server Component
import Yazilar from '@/app/ui/yazilar'
import { Suspense } from 'react'
 
export default function Page() {
  const yazilar = getYazilar() // await etme
  return (
    <Suspense fallback={<div>Yükleniyor...</div>}>
      <Yazilar yazilar={yazilar} />
    </Suspense>
  )
}
 
// app/ui/yazilar.tsx — Client Component
'use client'
import { use } from 'react'
 
export default function Yazilar({ yazilar }: { yazilar: Promise<Yazi[]> }) {
  const tumYazilar = use(yazilar) // promise'i çözer, hazır olana kadar askıya alır
  return <ul>{tumYazilar.map(y => <li key={y.id}>{y.baslik}</li>)}</ul>
}

5. Önbellekleme ve Yeniden Doğrulama

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.

use cache Direktifi

Dönüş değerini önbelleğe almak için herhangi bir async fonksiyon veya bileşene 'use cache' ekle:

// Veri seviyesi önbellekleme
import { cacheLife } from 'next/cache'
 
export async function getUrunler() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM urunler')
}
 
// UI seviyesi önbellekleme — tüm bileşeni önbelleğe al
export default async function UrunListesi() {
  'use cache'
  cacheLife('hours')
 
  const urunler = await db.query('SELECT * FROM urunler')
  return (
    <ul>
      {urunler.map(u => <li key={u.id}>{u.ad}</li>)}
    </ul>
  )
}
 
// Dosya seviyesi önbellekleme — dosyadaki tüm export'lar önbelleğe alınır
'use cache'
 
export async function getKullanici(id: string) {
  cacheLife('days')
  return db.kullanicilar.findById(id)
}

Argümanlar ve kapalı değerler otomatik olarak önbellek anahtarının parçası olur. Farklı girişler ayrı önbellek girdileri üretir.

cacheLife Profilleri

import { cacheLife } from 'next/cache'
 
// Yerleşik profiller
cacheLife('seconds') // bayat: 0,   yenile: 1s,  sona er: 60s
cacheLife('minutes') // bayat: 5d,  yenile: 1d,  sona er: 1s
cacheLife('hours')   // bayat: 5d,  yenile: 1s,  sona er: 1g
cacheLife('days')    // bayat: 5d,  yenile: 1g,  sona er: 1h
cacheLife('weeks')   // bayat: 5d,  yenile: 1h,  sona er: 30g
cacheLife('max')     // bayat: 5d,  yenile: 30g, sona er: ~süresiz
 
// Özel
cacheLife({
  stale: 3600,      // 1 saate kadar bayat sun
  revalidate: 7200, // 2 saatte yenile
  expire: 86400,    // 1 günde sona er
})

Kısa ömürlü önbellekler (seconds profili, revalidate: 0 veya expire < 5 dakika) otomatik olarak ön işlemelerden dışlanır ve dinamik deliklere dönüşür.

cacheTag ile İsteğe Bağlı Geçersiz Kılma

Önbelleğe alınan veriyi etiketle, böylece açıkça geçersiz kılabilirsin:

import { cacheTag } from 'next/cache'
 
export async function getUrunler() {
  'use cache'
  cacheTag('urunler')
  cacheLife('hours')
  return db.query('SELECT * FROM urunler')
}
 
export async function getUrun(id: string) {
  'use cache'
  cacheTag('urunler', `urun-${id}`) // birden fazla etiket
  cacheLife('days')
  return db.urunler.findById(id)
}

revalidateTag ve updateTag Farkı

İkisi benzer görünür ama farklı semantiğe sahip:

revalidateTagupdateTag
NeredeServer Actions + Route HandlerYalnızca Server Actions
DavranışStale-while-revalidate — bayat hemen sun, taze arka plandaÖnbelleği hemen sona erdirir — kullanıcı bir sonraki istekte taze görür
KullanımBlog yazıları, ürün katalogları — hafif gecikme kabul edilebilirKullanı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 uygundur
export 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örmelidir
export async function profilGuncelle(data: ProfilVerisi) {
  'use server'
  await db.profiller.guncelle(data)
  updateTag('profil') // hemen sona erdirir, sonraki istek taze olur
  redirect('/profil')
}

revalidatePath

Belirli bir rota yolu için tüm önbelleğe alınan veriyi geçersiz kıl. Mümkün olduğunda etiket tabanlı yeniden doğrulamayı tercih et — daha hassas.

import { revalidatePath } from 'next/cache'
 
export async function yaziOlustur() {
  'use server'
  await db.yazilar.olustur(...)
  revalidatePath('/yazilar') // /yazilar rotasını geçersiz kıl
}

refresh() — Etiketleri Yeniden Doğrulamadan Yenile

Mutasyondan sonra tam önbellek geçersiz kılımı olmadan mevcut sayfanın UI'sini yenile:

import { refresh } from 'next/cache'
 
export async function yaziGuncelle(formData: FormData) {
  'use server'
  await db.yazilar.guncelle(formData.get('id'), formData.get('baslik'))
  refresh() // istemci yönlendiricisini yeniler — son state'i gösterir
}

refresh() istemci yönlendiricisini yeniler. revalidatePath() / revalidateTag() önbelleğe alınan veriyi geçersiz kılar. Önbelleğin temizlenmesi VE UI'nin yenilenmesi gerektiğinde ikisini birlikte kullan.

Önbelleğe Alınmamış Veriyi Akıtma

Her istekte taze veri gerektiren bileşenler için 'use cache' kullanma. Bunun yerine <Suspense> ile sar:

import { Suspense } from 'react'
 
async function CanlıEnvanter({ urunId }: { urunId: string }) {
  // 'use cache' yok — her istekte taze getirir
  const envanter = await db.envanter.findByUrun(urunId)
  return <p>Stokta {envanter.adet} adet</p>
}
 
export default function UrunSayfasi({ urunId }: { urunId: string }) {
  return (
    <div>
      <OnbellekliUrunBilgi urunId={urunId} /> {/* statik kabuk */}
      <Suspense fallback={<p>Stok kontrol ediliyor...</p>}>
        <CanlıEnvanter urunId={urunId} /> {/* istek zamanında akıtılır */}
      </Suspense>
    </div>
  )
}

Runtime API'leri Suspense İçinde Olmak Zorunda

cookies(), headers(), searchParams — yalnızca istek zamanında kullanılabilir. Bunlara erişen bileşenler <Suspense> içinde olmak zorunda:

import { cookies } from 'next/headers'
import { Suspense } from 'react'
 
async function KullaniciKarsilama() {
  const tema = (await cookies()).get('tema')?.value || 'acik'
  return <p>Temanız: {tema}</p>
}
 
export default function Page() {
  return (
    <>
      <h1>Panel</h1>
      <Suspense fallback={<p>Yükleniyor...</p>}>
        <KullaniciKarsilama /> {/* cookie'ye erişiyor — Suspense içinde olmalı */}
      </Suspense>
    </>
  )
}

Önbelleğe Alınan Fonksiyonlara Runtime Değerleri Geçirmek

Önce runtime değerlerini çıkar, sonra argüman olarak geç:

// Bileşen (önbelleğe alınmamış) runtime verisini okur
async function ProfilIcerigi() {
  const session = (await cookies()).get('session')?.value
  return <OnbellekliProfil sessionId={session} />
}
 
// Önbelleğe alınan bileşen değeri prop olarak alır — sessionId önbellek anahtarı olur
async function OnbellekliProfil({ sessionId }: { sessionId: string }) {
  'use cache'
  cacheLife('minutes')
  const data = await kullaniciVerisiGetir(sessionId)
  return <div>{data.ad}</div>
}

Önceki Model (Cache Components olmadan)

Cache Components kullanmıyorsan eski fetch() semantiği hâlâ çalışır:

// Süresiz önbellek
const data = await fetch(url, { cache: 'force-cache' })
 
// Önbellek yok
const data = await fetch(url, { cache: 'no-store' })
 
// ISR
const data = await fetch(url, { next: { revalidate: 3600 } })
 
// Etiketli
const data = await fetch(url, { next: { tags: ['urunler'] } })

Fetch dışı veri kaynakları için unstable_cache:

import { unstable_cache } from 'next/cache'
 
export const onbellekliKullaniciyiAl = unstable_cache(
  async (id: string) => db.kullanicilar.findById(id),
  ['kullanici'],
  { tags: ['kullanici'], revalidate: 3600 }
)

6. Server Actions

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.

Temel Kalıp

'use server'
 
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import { auth } from '@/lib/auth'
 
const YaziOlusturmaShemasi = z.object({
  baslik: z.string().min(1).max(200),
  icerik: z.string().min(10),
})
 
export async function yaziOlustur(formData: FormData) {
  // 1. Kimlik doğrula
  const session = await auth()
  if (!session?.user) throw new Error('Yetkisiz')
 
  // 2. Doğrula
  const sonuc = YaziOlusturmaShemasi.safeParse({
    baslik: formData.get('baslik'),
    icerik: formData.get('icerik'),
  })
  if (!sonuc.success) {
    return { hata: sonuc.error.flatten().fieldErrors }
  }
 
  // 3. Yetkilendir — kullanıcının bu belirli işlemi yapabildiğini kontrol et
  // (yalnızca giriş yapmış olması yetmez)
 
  // 4. Çalıştır
  const yazi = await db.yazilar.olustur({
    data: { ...sonuc.data, yazarId: session.user.id },
  })
 
  // 5. Yeniden doğrula + yönlendir
  revalidatePath('/yazilar')
  redirect(`/yazilar/${yazi.id}`)
}

Formlarla Aşamalı Geliştirme

Server Actions JavaScript olmadan çalışır — form POST ile normal gönderilir:

// app/yazilar/yeni/page.tsx
import { yaziOlustur } from './actions'
 
export default function YeniYaziSayfasi() {
  return (
    <form action={yaziOlustur}>
      <input name="baslik" type="text" required />
      <textarea name="icerik" required />
      <button type="submit">Yazi Oluştur</button>
    </form>
  )
}

JS etkinken Next.js gönderimi ele geçirir, tam sayfa yenilemesi olmadan action'ı çağırır.

useActionState ile Bekleyen Durumlar

'use client'
 
import { useActionState } from 'react'
import { yaziOlustur } from './actions'
 
export function YaziOlusturmaFormu() {
  const [durum, action, bekliyor] = useActionState(yaziOlustur, null)
 
  return (
    <form action={action}>
      <input name="baslik" type="text" />
      {durum?.hata?.baslik && (
        <p className="text-red-500">{durum.hata.baslik[0]}</p>
      )}
      <textarea name="icerik" />
      {durum?.hata?.icerik && (
        <p className="text-red-500">{durum.hata.icerik[0]}</p>
      )}
      <button type="submit" disabled={bekliyor}>
        {bekliyor ? 'Oluşturuluyor...' : 'Yazi Oluştur'}
      </button>
    </form>
  )
}
 
// Güncellenen action — hata durumunda yönlendirmek yerine state döner
'use server'
 
export async function yaziOlustur(oncekiDurum: unknown, formData: FormData) {
  const sonuc = YaziOlusturmaShemasi.safeParse({
    baslik: formData.get('baslik'),
    icerik: formData.get('icerik'),
  })
  if (!sonuc.success) {
    return { hata: sonuc.error.flatten().fieldErrors }
  }
  // ... yazi olustur
  revalidatePath('/yazilar')
  redirect('/yazilar')
}

Satır İçi Action'lar

// app/yazilar/[id]/page.tsx
export default async function YaziSayfasi({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const yazi = await getYazi(id)
 
  async function yaziSil() {
    'use server'
    await db.yazilar.delete(yazi.id)
    revalidatePath('/yazilar')
    redirect('/yazilar')
  }
 
  return (
    <div>
      <h1>{yazi.baslik}</h1>
      <form action={yaziSil}>
        <button type="submit">Sil</button>
      </form>
    </div>
  )
}

Güvenlik: Her Zaman Kaynak Sahipliğini 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')
}

Server Actions vs Route Handler'lar

Server ActionsRoute Handler'lar
KullanımUI'dan mutasyonlarWebhook'lar, harici API'ler, UI olmayan istemciler
AuthOturum tabanlıToken/imza tabanlı
Aşamalı geliştirmeEvetHayır
Yeniden doğrulamaYerleşikManuel

7. Proxy

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.

Temel Bilgiler

// proxy.ts — proje kökünde (veya /src) olmak zorunda
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  return NextResponse.next()
}
 
// Alternatif olarak, default export
export default function proxy(request: NextRequest) {
  return NextResponse.next()
}
 
// Yalnızca belirli yollarda çalıştır
export const config = {
  matcher: [
    '/panel/:path*',
    '/api/:path*',
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

Auth Kalıbı

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.ts
import { 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.tsx
import { 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} />
}

Çok Kiracılı Yönlendirme

// proxy.ts
export function proxy(request: NextRequest) {
  const hostname = request.headers.get('host') ?? ''
  const altalan = hostname.split('.')[0]
 
  if (altalan === 'www' || altalan === 'app') {
    return NextResponse.next()
  }
 
  // Kiracı alt alanlarını dahili olarak yeniden yaz
  const url = request.nextUrl.clone()
  url.pathname = `/kiracı/${altalan}${url.pathname}`
  return NextResponse.rewrite(url)
}
musteri.uygulamaniz.com/panel
  ↓ proxy dahili olarak yeniden yazar
/kiracı/musteri/panel
  ↓ app/kiracı/[slug]/panel/page.tsx

CSP Nonce'ları

Content Security Policy için her istek başına taze nonce üret:

// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV === 'development'
 
  const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    object-src 'none';
  `.replace(/\s{2,}/g, ' ').trim()
 
  const istek Basliklar = new Headers(request.headers)
  istekBasliklar.set('x-nonce', nonce)
  istekBasliklar.set('Content-Security-Policy', csp)
 
  const yanit = NextResponse.next({ request: { headers: istekBasliklar } })
  yanit.headers.set('Content-Security-Policy', csp)
  return yanit
}

Edge Runtime Kısıtları

Proxy Edge Runtime üzerinde çalışır. Şunları kullanamazsın:

❌ Node.js yerleşikleri (fs, crypto, path, child_process)
❌ Node.js API'lerine bağımlı npm paketleri
❌ ~1MB üzeri paketler

✅ Web API'leri (fetch, URL, Headers, Request, Response)
✅ WebCrypto (SubtleCrypto)
✅ jose (JWT), zod, nanoid
✅ next/headers, next/cookies (kısıtlamalarla)

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.


8. Kalıplar

Tam Auth Akışı

// lib/session.ts
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'
import { cache } from 'react'
 
const JWT_SIRRI = new TextEncoder().encode(process.env.JWT_SECRET!)
 
export async function oturumOlustur(kullaniciId: string, rol: string) {
  const token = await new SignJWT({ sub: kullaniciId, rol })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .setIssuedAt()
    .sign(JWT_SIRRI)
 
  const cerezler = await cookies()
  cerezler.set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  })
}
 
export const getOturum = cache(async () => {
  const cerezler = await cookies()
  const token = cerezler.get('session')?.value
  if (!token) return null
 
  try {
    const { payload } = await jwtVerify(token, JWT_SIRRI)
    return payload as { sub: string; rol: string }
  } catch {
    return null
  }
})
 
export async function oturumSil() {
  const cerezler = await cookies()
  cerezler.delete('session')
}
// app/actions/auth.ts
'use server'
 
import { z } from 'zod'
import { oturumOlustur, oturumSil } from '@/lib/session'
import { redirect } from 'next/navigation'
 
const GirisShemasi = z.object({
  eposta: z.string().email(),
  sifre: z.string().min(8),
})
 
export async function girisYap(oncekiDurum: unknown, formData: FormData) {
  const sonuc = GirisShemasi.safeParse({
    eposta: formData.get('eposta'),
    sifre: formData.get('sifre'),
  })
  if (!sonuc.success) return { hata: 'Geçersiz e-posta veya şifre' }
 
  const kullanici = await db.kullanicilar.findByEposta(sonuc.data.eposta)
  if (!kullanici) return { hata: 'Geçersiz e-posta veya şifre' }
 
  const gecerli = await sifreDogrula(sonuc.data.sifre, kullanici.sifreHash)
  if (!gecerli) return { hata: 'Geçersiz e-posta veya şifre' }
 
  await oturumOlustur(kullanici.id, kullanici.rol)
  redirect('/panel')
}
 
export async function cikisYap() {
  await oturumSil()
  redirect('/giris')
}

useOptimistic ile İyimser UI

'use client'
 
import { useOptimistic, useTransition } from 'react'
import { begeniToggle } from './actions'
 
export function BegeniButonu({
  yazi,
}: {
  yazi: { id: string; begeniler: number; kullaniciBegendi: boolean }
}) {
  const [iyimserYazi, iyimserGuncellemeEkle] = useOptimistic(
    yazi,
    (durum, begenildi: boolean) => ({
      ...durum,
      begeniler: begenildi ? durum.begeniler + 1 : durum.begeniler - 1,
      kullaniciBegendi: begenildi,
    })
  )
  const [bekliyor, startTransition] = useTransition()
 
  const handleToggle = () => {
    const begenilecek = !iyimserYazi.kullaniciBegendi
    startTransition(async () => {
      iyimserGuncellemeEkle(begenilecek) // anında UI güncellemesi
      await begeniToggle(yazi.id)        // gerçek sunucu çağrısı
    })
  }
 
  return (
    <button onClick={handleToggle} disabled={bekliyor}>
      {iyimserYazi.kullaniciBegendi ? '❤️' : '🤍'} {iyimserYazi.begeniler}
    </button>
  )
}

URL State Yönetimi

UI state'ini URL arama parametrelerinde sakla — paylaşılabilir, yenilemeye dayanıklı:

// hooks/useQueryState.ts
'use client'
 
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
import { useCallback, useTransition } from 'react'
 
export function useQueryState(anahtar: string, varsayilan = '') {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const [bekliyor, startTransition] = useTransition()
 
  const deger = searchParams.get(anahtar) ?? varsayilan
 
  const degerAyarla = useCallback((yeniDeger: string) => {
    const params = new URLSearchParams(searchParams.toString())
    if (yeniDeger === varsayilan) {
      params.delete(anahtar)
    } else {
      params.set(anahtar, yeniDeger)
    }
    startTransition(() => {
      router.replace(`${pathname}?${params.toString()}`, { scroll: false })
    })
  }, [anahtar, varsayilan, pathname, router, searchParams])
 
  return [deger, degerAyarla, bekliyor] as const
}

Çok Kiracılı Mimari

// app/kiracı/[slug]/layout.tsx
import { notFound } from 'next/navigation'
import { getKiracı } from '@/lib/kiracı'
 
export default async function KiracıLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const kiracı = await getKiracı(slug)
  if (!kiracı) notFound()
 
  return (
    <KiracıSağlayıcı kiracı={kiracı}>
      <KiracıTema birincilRenk={kiracı.markaRengi}>
        {children}
      </KiracıTema>
    </KiracıSağlayıcı>
  )
}

unstable_instant ile Anında Navigasyon

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:

// app/magaza/[slug]/page.tsx
import { Suspense } from 'react'
import { cacheLife } from 'next/cache'
 
export const unstable_instant = {
  prefetch: 'static',
  samples: [{ params: { slug: 'ornek-urun' } }],
}
 
export default function UrunSayfasi(props: PageProps<'/magaza/[slug]'>) {
  return (
    <div>
      <Suspense fallback={<UrunIskeleti />}>
        <UrunBilgi params={props.params} />
      </Suspense>
      <Suspense fallback={<EnvanterIskeleti />}>
        <Envanter params={props.params} />
      </Suspense>
    </div>
  )
}
 
async function UrunBilgi({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const urun = await getOnbellekliUrun(slug)
  return <h1>{urun.ad}</h1>
}
 
async function getOnbellekliUrun(slug: string) {
  'use cache'
  cacheLife('hours')
  return db.urunler.findBySlug(slug)
}
 
async function Envanter({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const envanterOgesi = await db.envanter.findBySlug(slug) // önbellek yok — her istek taze
  return <p>Stokta {envanterOgesi.adet} adet</p>
}

Geliştirme sırasında Next.js Suspense sınırı yapısını doğrular. Derleme sırasında samples, navigasyonları simüle etmek için örnek paramlar sağlar.

Hata Sınırları

// app/panel/error.tsx
'use client'
 
import { useEffect } from 'react'
 
export default function Hata({
  error,
  unstable_retry,
}: {
  error: Error & { digest?: string }
  unstable_retry: () => void // eski versiyonlarda `reset`'ti
}) {
  useEffect(() => {
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Bir şeyler ters gitti</h2>
      <button onClick={unstable_retry}>Tekrar dene</button>
    </div>
  )
}
// app/global-error.tsx — kök layout'taki hataları yakalar
'use client'
 
export default function GenelHata({
  error,
  unstable_retry,
}: {
  error: Error & { digest?: string }
  unstable_retry: () => void
}) {
  return (
    <html>
      <body>
        <h2>Kritik hata</h2>
        <button onClick={unstable_retry}>Tekrar dene</button>
      </body>
    </html>
  )
}

9. Üretim Kontrol Listesi

Performans

// ✅ Görseller — her zaman next/image kullan
import Image from 'next/image'
<Image src="/hero.jpg" width={1200} height={630} alt="Hero" priority />
 
// ✅ Fontlar — next/font kullan, sıfır layout kayması
import { Geist } from 'next/font/google'
const geist = Geist({ subsets: ['latin'] })
 
// ✅ Script'ler — üçüncü taraf için next/script kullan
import Script from 'next/script'
<Script src="https://analytics.ornek.com" strategy="lazyOnload" />
 
// ✅ Ağır bileşenler için dinamik import'lar
import dynamic from 'next/dynamic'
const AgirGrafik = dynamic(() => import('./AgirGrafik'), {
  loading: () => <GrafikIskeleti />,
  ssr: false, // tarayıcı API'leri kullanıyorsa
})

Güvenlik Başlıkları

// next.config.ts
const basliklar = async () => [
  {
    source: '/(.*)',
    headers: [
      { key: 'X-Frame-Options', value: 'DENY' },
      { key: 'X-Content-Type-Options', value: 'nosniff' },
      { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
      {
        key: 'Strict-Transport-Security',
        value: 'max-age=31536000; includeSubDomains',
      },
    ],
  },
]

Ortam Değişkenleri

// lib/env.ts — başlangıçta doğrula, hızlı başarısız ol
import { z } from 'zod'
 
const envShemasi = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NODE_ENV: z.enum(['development', 'test', 'production']),
  NEXT_PUBLIC_APP_URL: z.string().url(),
})
 
export const env = envShemasi.parse(process.env)

Metadata ve SEO

// app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  metadataBase: new URL('https://uygulamaniz.com'),
  title: { default: 'Uygulamanız', template: '%s | Uygulamanız' },
  description: 'Varsayılan açıklama',
  openGraph: { type: 'website', locale: 'tr_TR', siteName: 'Uygulamanız' },
  robots: { index: true, follow: true },
}
 
// app/blog/[slug]/page.tsx — dinamik metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const yazi = await getYazi(slug)
  if (!yazi) return {}
 
  return {
    title: yazi.baslik,
    description: yazi.ozet,
    openGraph: {
      images: [{ url: `/api/og?baslik=${encodeURIComponent(yazi.baslik)}` }],
    },
  }
}

Dinamik OG Görselleri

// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
 
export const runtime = 'edge'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const baslik = searchParams.get('baslik') ?? 'Varsayılan Başlık'
 
  return new ImageResponse(
    (
      <div style={{
        display: 'flex',
        width: '100%',
        height: '100%',
        background: '#0f172a',
        alignItems: 'center',
        justifyContent: 'center',
      }}>
        <h1 style={{ color: 'white', fontSize: 64 }}>{baslik}</h1>
      </div>
    ),
    { width: 1200, height: 630 }
  )
}

Gönderi Öncesi Kontrol Listesi

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

Ek: Hızlı Referans

Dosya Kuralları

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

Önbellekleme API Referansı

// use cache direktifi
'use cache'                          // fonksiyon veya bileşen gövdesinde
cacheLife('hours')                   // önbellek ömrü ayarla
cacheTag('urunler', 'one-cikan')     // geçersiz kılma için etiketle
 
// Geçersiz kılma
revalidateTag('etiket')              // stale-while-revalidate (Server Action veya Route Handler)
updateTag('etiket')                  // anında sona erdir (yalnızca Server Action)
revalidatePath('/yol')               // yola göre geçersiz kıl
refresh()                            // istemci yönlendiricisini yenile
 
// Rota segment config (page.tsx / layout.tsx)
export const dynamic = 'force-dynamic' | 'force-static' | 'auto'
export const revalidate = 60         // ISR saniye cinsinden
export const revalidate = false      // statik, asla yenileme
 
// Önceki model (cacheComponents olmadan)
fetch(url, { cache: 'force-cache' })
fetch(url, { cache: 'no-store' })
fetch(url, { next: { revalidate: 60 } })
fetch(url, { next: { tags: ['etiket'] } })
unstable_cache(fn, anahtarParcalari, { tags, revalidate })

Render Karar Ağacı

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.