Next.js 15 App Router ile gerçek bir full-stack uygulama — Server Components, Server Actions, Route Handlers ve Streaming ile modern web geliştirme.
App Router Mimarisi
// PLAINTEXT //
app/
├── layout.tsx ← Root layout (her sayfada ortak)
├── page.tsx ← Ana sayfa
├── loading.tsx ← Suspense fallback
├── error.tsx ← Hata sınırı
├── not-found.tsx ← 404 sayfası
├── api/
│ └── webhooks/route.ts ← Route Handler
├── blog/
│ ├── page.tsx ← /blog
│ ├── [slug]/
│ │ ├── page.tsx ← /blog/bir-makale
│ │ └── loading.tsx
│ └── layout.tsx ← Blog'a özgü layout
└── (auth)/ ← Gruplama (URL'e etki etmez)
├── login/page.tsx
└── register/page.tsxServer vs Client Components
// TSX //
// app/blog/page.tsx — Server Component (varsayılan)
// "use client" yoksa → server'da render edilir
// Bu bileşen doğrudan veritabanı çağrısı yapabilir
import { db } from '@/lib/db';
async function BlogSayfasi() {
// Doğrudan async/await — useEffect yok
const makaleler = await db.makale.findMany({
where: { yayinlandi: true },
orderBy: { olusturulduAt: 'desc' },
take: 10,
select: {
id: true, baslik: true, slug: true,
olusturulduAt: true,
yazar: { select: { ad: true } },
},
});
return (
<div>
{makaleler.map(makale => (
<MakaleKart key={makale.id} makale={makale} />
))}
</div>
);
}
export default BlogSayfasi;
// components/MakaleKart.tsx — Client Component
'use client';
import { useState } from 'react';
function MakaleKart({ makale }: { makale: Makale }) {
const [begendi, setBegendi] = useState(false);
return (
<article>
<h2>{makale.baslik}</h2>
<button onClick={() => setBegendi(!begendi)}>
{begendi ? 'Beğenildi ✓' : 'Beğen'}
</button>
</article>
);
}Server Actions — Form İşlemleri
// TSX //
// app/blog/yeni/page.tsx
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { auth } from '@/auth';
// Server Action — "use server" directive
async function makaleOlustur(formVeri: FormData) {
'use server';
const oturum = await auth();
if (!oturum?.user) throw new Error('Oturum açın');
const baslik = formVeri.get('baslik') as string;
const icerik = formVeri.get('icerik') as string;
if (!baslik || baslik.length < 5) {
throw new Error('Başlık en az 5 karakter olmalı');
}
const makale = await db.makale.create({
data: {
baslik,
slug: slugOlustur(baslik),
icerik,
yazarId: oturum.user.id,
},
});
revalidatePath('/blog'); // Önbelleği geçersiz kıl
redirect(`/blog/${makale.slug}`); // İlgili sayfaya yönlendir
}
export default function YeniMakaleSayfasi() {
return (
<form action={makaleOlustur}>
<input name="baslik" placeholder="Başlık" required />
<textarea name="icerik" rows={10} placeholder="İçerik..." />
<button type="submit">Yayınla</button>
</form>
);
}useFormState ve useFormStatus
// TSX //
// Server Action — validasyon hataları ile
'use server';
import { z } from 'zod';
const MakaleSchema = z.object({
baslik: z.string().min(5, 'En az 5 karakter').max(300),
icerik: z.string().min(100, 'En az 100 karakter'),
});
export async function makaleOlusturAction(
oncekiDurum: ActionDurumu,
formVeri: FormData
): Promise<ActionDurumu> {
'use server';
const sonuc = MakaleSchema.safeParse({
baslik: formVeri.get('baslik'),
icerik: formVeri.get('icerik'),
});
if (!sonuc.success) {
return {
basarili: false,
hatalar: sonuc.error.flatten().fieldErrors,
};
}
await db.makale.create({ data: { ...sonuc.data } });
revalidatePath('/blog');
return { basarili: true };
}
// Client Component — form durumu takibi
'use client';
import { useFormState, useFormStatus } from 'react-dom';
function GonderButonu() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Kaydediliyor...' : 'Yayınla'}
</button>
);
}
function MakaleFormu() {
const [durum, gonder] = useFormState(makaleOlusturAction, { basarili: false });
return (
<form action={gonder}>
<input name="baslik" />
{durum.hatalar?.baslik && (
<p className="text-red-500">{durum.hatalar.baslik[0]}</p>
)}
<GonderButonu />
{durum.basarili && <p className="text-green-500">Makale yayınlandı!</p>}
</form>
);
}Streaming ile Kademeli Yükleme
// TSX //
// app/blog/page.tsx — Streaming
import { Suspense } from 'react';
async function YavasBilesenler() {
await new Promise(r => setTimeout(r, 2000)); // Yavaş veri
return <div>Ağır içerik yüklendi</div>;
}
export default function BlogSayfasi() {
return (
<div>
<h1>Blog</h1>
{/* Hızlı içerik hemen render */}
<HizliIcerik />
{/* Yavaş içerik — ayrı Suspense boundary */}
<Suspense fallback={<YuklemeSkeleti />}>
<YavasBilesenler />
</Suspense>
<Suspense fallback={<p>Yorumlar yükleniyor...</p>}>
<YorumListesi />
</Suspense>
</div>
);
}
// loading.tsx — otomatik Suspense
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
);
}Route Handlers ve Middleware
// TYPESCRIPT //
// app/api/blog/[slug]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
) {
const makale = await db.makale.findUnique({
where: { slug: params.slug, yayinlandi: true },
include: { yazar: { select: { ad: true } } },
});
if (!makale) {
return NextResponse.json({ hata: 'Bulunamadı' }, { status: 404 });
}
return NextResponse.json(makale, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
// middleware.ts — her istekte çalışır
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
const korunanRotalar = ['/admin', '/dashboard'];
const korunanRota = korunanRotalar.some(rota =>
request.nextUrl.pathname.startsWith(rota)
);
if (korunanRota && !token) {
return NextResponse.redirect(new URL('/giris', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};generateStaticParams ile SSG
// TYPESCRIPT //
// app/blog/[slug]/page.tsx
import { db } from '@/lib/db';
import { notFound } from 'next/navigation';
// Build zamanı: tüm slug'ları çekip statik sayfa oluştur
export async function generateStaticParams() {
const makaleler = await db.makale.findMany({
where: { yayinlandi: true },
select: { slug: true },
});
return makaleler.map(m => ({ slug: m.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const makale = await db.makale.findUnique({ where: { slug: params.slug } });
if (!makale) return { title: 'Bulunamadı' };
return {
title: makale.baslik,
description: makale.ozet,
openGraph: {
title: makale.baslik,
description: makale.ozet,
type: 'article',
},
};
}
async function MakaleSayfasi({ params }: { params: { slug: string } }) {
const makale = await db.makale.findUnique({
where: { slug: params.slug, yayinlandi: true },
include: { yazar: true },
});
if (!makale) notFound();
return (
<article>
<h1>{makale.baslik}</h1>
<p>Yazar: {makale.yazar.ad}</p>
<div dangerouslySetInnerHTML={{ __html: makale.icerikHtml }} />
</article>
);
}
export default MakaleSayfasi;Sonuç
Next.js 15 App Router, Server Components ile sıfır JavaScript gönderimi, Server Actions ile form işlemleri, Streaming ile kademeli yükleme ve generateStaticParams ile SSG sağlar. Bu modüler yapı; performanslı, SEO dostu ve geliştirici deneyimi yüksek full-stack uygulamalar oluşturmanıza olanak tanır.