Next.js 15 App Router Derinlemesine: Modern Web Uygulamaları İçin Kapsamlı Rehber
Next.js 15 App Router ile modern web uygulamaları geliştirmenin tüm inceliklerini öğrenin. Dosya tabanlı routing, layout sistemi, loading states, error handling ve production best practices bu kapsamlı rehberde.
Next.js 15 App Router Derinlemesine: Modern Web Uygulamaları İçin Kapsamlı Rehber
Next.js 15, Ekim 2024'te yayınlandığında web development camiasında ciddi bir heyecan yarattı. App Router artık tamamen olgunlaşmış durumda ve Pages Router'ın yerini almaya hazır. Peki App Router neden bu kadar önemli? Çünkü React Server Components, nested layouts, streaming ve parallel routes gibi modern özellikleri native olarak destekliyor.
Eğer hâlâ Pages Router kullanıyorsanız veya Next.js'e yeni başlıyorsanız, bu rehber tam size göre. App Router'ın temellerinden ileri seviye pattern'lere kadar her şeyi ele alacağız. Gerçek dünya örnekleriyle, production-ready kod snippetleriyle ve kaçınılması gereken hatalarla birlikte.
İçindekiler
- App Router Nedir ve Neden Önemli?
- Dosya Tabanlı Routing Sistemi
- Layout ve Template Yapısı
- Server ve Client Components
- Loading States ve Suspense
- Error Handling Stratejileri
- Parallel ve Intercepting Routes
- Route Groups ve Organizasyon
- Metadata ve SEO Optimizasyonu
- Data Fetching Patterns
- Caching ve Revalidation
- Production Best Practices
- Sık Sorulan Sorular
App Router Nedir ve Neden Önemli?
App Router, Next.js 13'te tanıtılan ve Next.js 15'te tamamen olgunlaşan yeni routing sistemi. Pages Router'dan farklı olarak app/ dizininde yaşıyor ve React'in en yeni özelliklerini kullanıyor.
Pages Router vs App Router
| Özellik | Pages Router | App Router |
|---|---|---|
| Dizin | pages/ | app/ |
| Componentler | Client-only | Server-first |
| Layouts | Her sayfada tekrar | Nested, persist |
| Data Fetching | getServerSideProps | async components |
| Streaming | Yok | Native support |
| Parallel Routes | Yok | Var |
Rakamlarla konuşalım. Vercel'in yaptığı benchmarklara göre App Router:
- %40 daha hızlı cold start süreleri
- %60 daha az client-side JavaScript
- %35 iyileşme Time to First Byte'ta
Bu rakamlar teorik değil. E-commerce sitelerinde, SaaS dashboard'larında ve content-heavy portallerde gözlemlenen gerçek iyileştirmeler.
Temel Farklar
// Pages Router - pages/products/[id].js
export async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id)
return { props: { product } }
}
export default function ProductPage({ product }) {
return <h1>{product.name}</h1>
}
// App Router - app/products/[id]/page.jsx
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id)
return <h1>{product.name}</h1>
}App Router'da data fetching direkt component içinde yapılıyor. Ayrı bir fonksiyon yok, props drilling yok. Component async olabiliyor ve sunucuda çalışıyor.
Dosya Tabanlı Routing Sistemi
App Router'ın routing sistemi dosya ve klasör yapısına dayanıyor. Her klasör bir route segment'i, özel dosyalar ise belirli davranışları temsil ediyor.
Özel Dosyalar
| Dosya | Amaç |
|---|---|
page.jsx | Route'un UI'ı, publicly accessible yapar |
layout.jsx | Shared UI, re-render olmaz |
template.jsx | Layout gibi ama her navigasyonda re-render |
loading.jsx | Loading UI, Suspense boundary |
error.jsx | Error UI, Error boundary |
not-found.jsx | 404 UI |
route.js | API endpoint |
Klasör Yapısı
app/
├── page.jsx # / (anasayfa)
├── layout.jsx # Root layout
├── about/
│ └── page.jsx # /about
├── blog/
│ ├── page.jsx # /blog
│ └── [slug]/
│ └── page.jsx # /blog/:slug
├── products/
│ ├── page.jsx # /products
│ ├── [id]/
│ │ ├── page.jsx # /products/:id
│ │ └── reviews/
│ │ └── page.jsx # /products/:id/reviews
│ └── [...categories]/
│ └── page.jsx # /products/* (catch-all)
└── (marketing)/
├── pricing/
│ └── page.jsx # /pricing
└── contact/
└── page.jsx # /contact
Dynamic Routes
// app/blog/[slug]/page.jsx
export default async function BlogPost({ params }) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// Static generation için
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}Catch-All Routes
// app/docs/[...slug]/page.jsx
export default async function DocsPage({ params }) {
const { slug } = await params
// slug = ['getting-started', 'installation'] for /docs/getting-started/installation
const doc = await getDoc(slug.join('/'))
return <DocContent content={doc} />
}
// Optional catch-all: [[...slug]]
// app/shop/[[...categories]]/page.jsx
// Matches /shop, /shop/electronics, /shop/electronics/phonesLayout ve Template Yapısı
Layout sistemi App Router'ın en güçlü özelliklerinden biri. Nested layouts sayesinde kod tekrarını önlüyor ve state'i koruyorsunuz.
Root Layout
// app/layout.jsx
import { Inter, Space_Grotesk } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
variable: '--font-space'
})
export const metadata = {
title: {
default: 'My App',
template: '%s | My App'
},
description: 'Modern web uygulaması'
}
export default function RootLayout({ children }) {
return (
<html lang="tr" className={`${inter.variable} ${spaceGrotesk.variable}`}>
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
)
}Root layout zorunlu ve <html> ile <body> taglarını içermeli.
Nested Layouts
// app/dashboard/layout.jsx
import { Sidebar } from '@/components/sidebar'
import { DashboardHeader } from '@/components/dashboard-header'
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<div className="flex-1">
<DashboardHeader />
<div className="p-6">{children}</div>
</div>
</div>
)
}
// app/dashboard/analytics/page.jsx
export default function AnalyticsPage() {
// Bu sayfa otomatik olarak DashboardLayout içinde render edilir
return <AnalyticsDashboard />
}
// app/dashboard/settings/page.jsx
export default function SettingsPage() {
// Bu da aynı layout içinde, sidebar persist eder
return <SettingsForm />
}Kullanıcı /dashboard/analytics'ten /dashboard/settings'e gittiğinde sidebar yeniden render edilmez. State korunur, sadece children değişir.
Template vs Layout
// app/blog/template.jsx
export default function BlogTemplate({ children }) {
// Her navigasyonda yeni instance oluşur
// Animation'lar için idealdir
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
{children}
</motion.div>
)
}Template her route değişikliğinde yeniden mount olur. Entry/exit animasyonları için kullanışlı.
Server ve Client Components
Next.js 15'te tüm componentler varsayılan olarak Server Component. Interaktivite gerektiğinde 'use client' directive kullanıyorsunuz.
Server Component Avantajları
// app/products/page.jsx - Server Component
import { db } from '@/lib/database'
import { formatPrice } from '@/lib/utils'
export default async function ProductsPage() {
// Direkt database erişimi
const products = await db.product.findMany({
include: { category: true, reviews: true }
})
// Pahalı hesaplama sunucuda
const stats = products.map(p => ({
...p,
avgRating: p.reviews.reduce((a, r) => a + r.rating, 0) / p.reviews.length,
formattedPrice: formatPrice(p.price)
}))
return (
<div className="grid grid-cols-3 gap-6">
{stats.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}Bu component için hiç JavaScript client'a gönderilmez. Database credentials güvende, bundle size minimal.
Client Component Kullanımı
// components/add-to-cart-button.jsx
'use client'
import { useState } from 'react'
import { useCart } from '@/hooks/use-cart'
export function AddToCartButton({ productId, price }) {
const [loading, setLoading] = useState(false)
const { addItem } = useCart()
async function handleClick() {
setLoading(true)
await addItem(productId)
setLoading(false)
}
return (
<button
onClick={handleClick}
disabled={loading}
className="btn-primary"
>
{loading ? 'Ekleniyor...' : `Sepete Ekle - ${price}`}
</button>
)
}Composition Pattern
// app/products/[id]/page.jsx - Server Component
import { AddToCartButton } from '@/components/add-to-cart-button'
import { ProductGallery } from '@/components/product-gallery'
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div className="grid grid-cols-2 gap-8">
{/* Client Component - interaktif gallery */}
<ProductGallery images={product.images} />
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component - click handler */}
<AddToCartButton
productId={product.id}
price={product.formattedPrice}
/>
</div>
</div>
)
}Server componentler client componentleri içerebilir ama tersi mümkün değil (children prop hariç).
Loading States ve Suspense
App Router, React Suspense'i native olarak destekliyor. loading.jsx dosyası otomatik Suspense boundary oluşturuyor.
Loading UI
// app/dashboard/loading.jsx
export default function DashboardLoading() {
return (
<div className="space-y-4">
<div className="h-8 w-64 bg-gray-200 rounded animate-pulse" />
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded animate-pulse" />
))}
</div>
</div>
)
}
// app/dashboard/page.jsx
export default async function DashboardPage() {
const data = await fetchDashboardData() // Yavaş olabilir
return <Dashboard data={data} />
}Kullanıcı /dashboard'a gittiğinde önce loading UI gösterilir, data hazır olunca gerçek içerik.
Granular Loading States
// app/dashboard/page.jsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="space-y-6">
{/* Hızlı yüklenen kısım */}
<DashboardHeader />
{/* Ayrı Suspense boundary'ler */}
<div className="grid grid-cols-2 gap-6">
<Suspense fallback={<StatsSkeleton />}>
<StatsWidget />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}
// Her widget kendi hızında yüklenir
async function StatsWidget() {
const stats = await fetchStats()
return <Stats data={stats} />
}
async function RevenueChart() {
const revenue = await fetchRevenue()
return <Chart data={revenue} />
}Bu pattern "streaming" olarak bilinir. Her bölüm hazır olduğunda client'a gönderilir, tüm sayfayı beklemek gerekmez.
Error Handling Stratejileri
App Router, React Error Boundaries'i dosya sistemi üzerinden yönetiyor.
Error UI
// app/dashboard/error.jsx
'use client' // Error componentler client component olmalı
import { useEffect } from 'react'
export default function DashboardError({ error, reset }) {
useEffect(() => {
// Error logging service'e gönder
console.error('Dashboard error:', error)
}, [error])
return (
<div className="p-8 text-center">
<h2 className="text-xl font-bold text-red-600">
Bir şeyler yanlış gitti
</h2>
<p className="mt-2 text-gray-600">
Dashboard yüklenirken hata oluştu.
</p>
<button
onClick={() => reset()}
className="mt-4 btn-primary"
>
Tekrar Dene
</button>
</div>
)
}reset() fonksiyonu error boundary'i sıfırlar ve segment'i yeniden render etmeyi dener.
Global Error Handler
// app/global-error.jsx
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold">500</h1>
<p className="mt-4">Sunucu hatası oluştu</p>
<button onClick={() => reset()} className="mt-4 btn">
Yenile
</button>
</div>
</div>
</body>
</html>
)
}Global error root layout'taki hataları yakalar. Kendi <html> ve <body> taglarını tanımlamalı.
Not Found
// app/not-found.jsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-primary">404</h1>
<h2 className="mt-4 text-2xl">Sayfa Bulunamadı</h2>
<p className="mt-2 text-gray-600">
Aradığınız sayfa mevcut değil veya taşınmış olabilir.
</p>
<Link href="/" className="mt-6 inline-block btn-primary">
Anasayfaya Dön
</Link>
</div>
</div>
)
}
// Programmatic olarak tetikleme
import { notFound } from 'next/navigation'
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
if (!product) {
notFound() // not-found.jsx render edilir
}
return <Product data={product} />
}Parallel ve Intercepting Routes
Advanced routing pattern'leri. Dashboard'lar ve modal'lar için güçlü araçlar.
Parallel Routes
app/
├── layout.jsx
├── page.jsx
└── dashboard/
├── layout.jsx
├── @analytics/
│ └── page.jsx
├── @team/
│ └── page.jsx
└── @notifications/
└── page.jsx
// app/dashboard/layout.jsx
export default function DashboardLayout({
children,
analytics,
team,
notifications
}) {
return (
<div className="grid grid-cols-12 gap-6">
<div className="col-span-8">{children}</div>
<div className="col-span-4 space-y-6">
{analytics}
{team}
{notifications}
</div>
</div>
)
}Her slot bağımsız loading ve error state'ine sahip olabilir. Birinin yavaş yüklenmesi diğerlerini etkilemez.
Intercepting Routes
Modal pattern için idealdir. URL değişir ama modal içinde gösterilir.
app/
├── feed/
│ └── page.jsx
├── photo/
│ └── [id]/
│ └── page.jsx
└── @modal/
└── (.)photo/
└── [id]/
└── page.jsx
// app/@modal/(.)photo/[id]/page.jsx
import { Modal } from '@/components/modal'
export default async function PhotoModal({ params }) {
const photo = await getPhoto(params.id)
return (
<Modal>
<img src={photo.url} alt={photo.alt} />
<p>{photo.description}</p>
</Modal>
)
}Feed'den photo'ya tıklandığında modal açılır ama URL /photo/123 olur. Refresh yapılırsa tam sayfa versiyonu gösterilir.
Route Groups ve Organizasyon
Parantez içindeki klasörler URL'e yansımaz, sadece organizasyon amaçlıdır.
app/
├── (marketing)/
│ ├── layout.jsx # Marketing layout
│ ├── page.jsx # / (anasayfa)
│ ├── about/
│ │ └── page.jsx # /about
│ └── pricing/
│ └── page.jsx # /pricing
├── (shop)/
│ ├── layout.jsx # Shop layout (sidebar ile)
│ ├── products/
│ │ └── page.jsx # /products
│ └── cart/
│ └── page.jsx # /cart
└── (auth)/
├── layout.jsx # Minimal auth layout
├── login/
│ └── page.jsx # /login
└── register/
└── page.jsx # /register
Her grup kendi layout'una sahip olabilir. Marketing sayfaları full-width, shop sayfaları sidebar'lı, auth sayfaları minimal.
Metadata ve SEO Optimizasyonu
App Router güçlü metadata API'si sunuyor.
Static Metadata
// app/about/page.jsx
export const metadata = {
title: 'Hakkımızda',
description: 'Şirketimiz hakkında detaylı bilgi',
openGraph: {
title: 'Hakkımızda | Şirket Adı',
description: 'Şirketimiz hakkında detaylı bilgi',
images: ['/og-about.jpg']
},
twitter: {
card: 'summary_large_image',
title: 'Hakkımızda',
description: 'Şirketimiz hakkında detaylı bilgi'
}
}Dynamic Metadata
// app/blog/[slug]/page.jsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name]
},
alternates: {
canonical: `https://example.com/blog/${params.slug}`
}
}
}JSON-LD Schema
// app/blog/[slug]/page.jsx
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
author: {
'@type': 'Person',
name: post.author.name
}
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>
<h1>{post.title}</h1>
{/* ... */}
</article>
</>
)
}Data Fetching Patterns
App Router'da data fetching çeşitli yollarla yapılabilir.
Async Components
// app/products/page.jsx
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // 1 saat cache
}).then(r => r.json())
return <ProductGrid products={products} />
}Parallel Data Fetching
export default async function DashboardPage() {
// Paralel fetch - waterfall yok
const [user, orders, analytics] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchAnalytics()
])
return (
<Dashboard user={user} orders={orders} analytics={analytics} />
)
}Sequential with Dependency
export default async function ProfilePage({ params }) {
// User'ı fetch et
const user = await fetchUser(params.id)
// User'a bağlı data'yı fetch et
const [posts, followers] = await Promise.all([
fetchUserPosts(user.id),
fetchFollowers(user.id)
])
return <Profile user={user} posts={posts} followers={followers} />
}Caching ve Revalidation
Next.js 15'te caching varsayılan olarak kapalı (önceki sürümlerden farklı).
Fetch Caching
// Cache'siz (varsayılan)
const data = await fetch('https://api.example.com/data')
// Cache ile
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
})
// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // 60 saniye
})Route Segment Config
// app/products/page.jsx
export const revalidate = 3600 // Tüm segment için 1 saat
export const dynamic = 'force-dynamic' // Her zaman dynamic
export const fetchCache = 'force-cache' // Fetch'leri cache'leOn-Demand Revalidation
// app/actions/revalidate.js
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updateProduct(id, data) {
await db.product.update({ where: { id }, data })
// Belirli path'i revalidate et
revalidatePath('/products')
revalidatePath(`/products/${id}`)
// Veya tag bazlı
revalidateTag('products')
}
// Fetch'te tag kullanımı
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
})Production Best Practices
1. Component Hierarchy
✅ Doğru:
app/
├── layout.jsx (Server)
├── page.jsx (Server)
└── components/
├── header.jsx (Server)
├── nav-links.jsx (Server)
└── mobile-menu.client.jsx (Client - sadece bu)
❌ Yanlış:
app/
├── layout.client.jsx (Gereksiz client)
├── page.client.jsx (Tüm sayfa client)
2. Image Optimization
import Image from 'next/image'
export function ProductCard({ product }) {
return (
<div>
<Image
src={product.image}
alt={product.name}
width={300}
height={300}
priority={false}
placeholder="blur"
blurDataURL={product.blurHash}
/>
</div>
)
}3. Font Optimization
// app/layout.jsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter'
})
export default function RootLayout({ children }) {
return (
<html lang="tr" className={inter.variable}>
<body>{children}</body>
</html>
)
}4. Bundle Analysis
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer({
// config
})
# Çalıştır
ANALYZE=true npm run buildSık Sorulan Sorular
Pages Router'dan App Router'a geçmeli miyim?
Yeni projeler için kesinlikle App Router önerilir. Mevcut projeler için aşamalı migration yapabilirsiniz, iki sistem birlikte çalışabiliyor.
Server Actions production-ready mi?
Evet, Next.js 14'ten beri stable. Form handling, mutations ve data updates için güvenle kullanabilirsiniz.
App Router SEO için daha mı iyi?
Evet. Server Components sayesinde content crawl edilebilir, metadata API güçlü ve streaming ile Core Web Vitals iyileşiyor.
Hangi state management kullanmalıyım?
Server Components için state yok, data fetch edilir. Client Components için Context, Zustand veya Jotai önerilir. Redux hâlâ çalışır ama overhead fazla.
App Router ile API routes kullanabilir miyim?
Evet, app/api/ dizininde Route Handlers kullanabilirsiniz. Ancak Server Actions birçok use case'i kapsar.
Vercel dışında deploy edebilir miyim?
Evet. Node.js runtime'da self-host, Docker container veya Netlify, Cloudflare Pages gibi platformlarda çalışır.
Sonuç
Next.js 15 App Router, modern web development için güçlü bir temel sunuyor. Server Components ile performance, nested layouts ile DX, streaming ile UX - hepsi bir arada.
Öğrenme eğrisi var ama kazanımlar büyük. Eğer React ekosisteminde production uygulaması geliştiriyorsanız, App Router öğrenmek artık zorunluluk haline geldi.
Bu rehberdeki pattern'leri küçük bir projede deneyerek başlayın. Error handling, loading states ve data fetching pattern'lerini pratik yapın. Gerisi zamanla gelecek.
Sonraki Adımlar:
- Yeni bir Next.js 15 projesi oluşturun
- Nested layouts ile basit bir dashboard yapın
- Server Actions ile form handling deneyin
- Parallel routes ile kompleks UI oluşturun
- Production'a deploy edip performance ölçün
Serinin devamında API Routes, Database entegrasyonu ve Authentication konularını işleyeceğiz. Takipte kalın!
Projenizi Hayata Geçirelim
Web sitesi, mobil uygulama veya yapay zeka çözümü mü arıyorsunuz? Fikirlerinizi birlikte değerlendirelim.
Ücretsiz Danışmanlık Alın