REACT / NEXT.JS // FULL-STACKD::04 İLERİ
26m READCOMPLETION: 78%ID::RCT-301

NEXT.JS 15 FULL-STACK: SERVER ACTIONS

App Router, Server Components, Server Actions ve generateStaticParams

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.tsx

Server 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.