Skip to content

Next.js 14/15 Complete Guide — App Router & TypeScript

📘 Practical reference guide for Next.js (App Router) with TypeScript.
(TypeScript ile Next.js (App Router) için pratik referans rehberi.)
React bilen full-stack geliştiriciler için hazırlanmıştır.

:::tip[Ne Zaman Next.js?] ✅ Ideal: SEO+SSR gereken projeler, full-stack React uygulamaları
⚠️ Gereksiz: Basit SPA için React + Vite yeterli
Uygun degil: React disindaki projeler (Vue, Svelte vb.)

Önerilen stack: Next.js + TypeScript + Prisma + Tailwind
Alternatifler: Nuxt (Vue), Remix (React), Astro (content-heavy) :::

1) Next.js Nedir?

Next.js is a React framework (not just a library) built by Vercel.
(Next.js, Vercel tarafından geliştirilen bir React framework'üdür — sadece kütüphane değil.)

Why Next.js? (Neden Next.js?)

  • File-based routing — no need for react-router (dosya tabanlı yönlendirme)
  • Server-Side Rendering (SSR) — SEO and performance (sunucu taraflı render)
  • Static Site Generation (SSG) — build-time HTML (derleme zamanında HTML)
  • API Routes — backend endpoints inside the same project (aynı projede backend)
  • Built-in optimizations — images, fonts, bundling (dahili optimizasyonlar)
  • TypeScript first-class support (TypeScript tam desteği)

Pages Router vs App Router

FeaturePages Router (eski)App Router (yeni, önerilen)
Directorypages/app/
ComponentsClient by defaultServer by default
Layouts_app.tsx, _document.tsxlayout.tsx (nested)
Data fetchinggetServerSideProps, getStaticPropsasync components, fetch
StreamingLimitedFull support with Suspense
Server ActionsNot availableBuilt-in

Bu rehber App Router (Next.js 14/15) odaklıdır.
Pages Router hala desteklenir ancak yeni projeler için App Router önerilir.


2) Kurulum & Proje Yapısı

Proje Oluşturma

bash
# Önerilen: Node.js 18.17+ gerekir
npx create-next-app@latest my-app

# Seçenekleri otomatik kabul et
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

cd my-app
npm run dev

Visit: http://localhost:3000

Dizin Yapısı (Directory Structure)

my-app/
├── src/
│   ├── app/                    # App Router root (ana dizin)
│   │   ├── layout.tsx          # Root layout (kök yerleşim)
│   │   ├── page.tsx            # Home page → "/"
│   │   ├── loading.tsx         # Loading UI (yüklenme arayüzü)
│   │   ├── error.tsx           # Error boundary (hata sınırı)
│   │   ├── not-found.tsx       # 404 page
│   │   ├── global-error.tsx    # Global error boundary
│   │   ├── favicon.ico
│   │   ├── globals.css
│   │   ├── about/
│   │   │   └── page.tsx        # "/about"
│   │   ├── blog/
│   │   │   ├── page.tsx        # "/blog"
│   │   │   └── [slug]/
│   │   │       └── page.tsx    # "/blog/hello-world"
│   │   └── api/
│   │       └── hello/
│   │           └── route.ts    # API: "/api/hello"
│   ├── components/             # Paylaşılan bileşenler
│   ├── lib/                    # Yardımcı fonksiyonlar
│   └── types/                  # TypeScript tipleri
├── public/                     # Statik dosyalar (resimler vs.)
├── next.config.ts              # Next.js yapılandırma
├── tailwind.config.ts
├── tsconfig.json
└── package.json

Özel Dosyalar (Special Files)

tsx
// app/layout.tsx — Root Layout (zorunlu)
// Her sayfayı saran kök yerleşim. <html> ve <body> burada tanımlanır.
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "My App",
  description: "Built with Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="tr">
      <body>{children}</body>
    </html>
  );
}
tsx
// app/page.tsx — Home Page
// Bu dosya "/" rotasına karşılık gelir
export default function HomePage() {
  return (
    <main>
      <h1>Merhaba Next.js!</h1>
    </main>
  );
}
tsx
// app/loading.tsx — Loading UI
// Sayfa yüklenirken otomatik gösterilir (React Suspense kullanır)
export default function Loading() {
  return <div className="animate-pulse">Yükleniyor...</div>;
}
tsx
// app/error.tsx — Error Boundary
// Hata oluştuğunda gösterilir. "use client" zorunludur.
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Bir hata oluştu!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Tekrar Dene</button>
    </div>
  );
}
tsx
// app/not-found.tsx — 404 Page
export default function NotFound() {
  return (
    <div>
      <h2>404 — Sayfa Bulunamadı</h2>
      <p>Aradığınız sayfa mevcut değil.</p>
    </div>
  );
}

3) Routing

Next.js uses file-based routing — folder structure = URL structure.
(Dosya tabanlı yönlendirme — klasör yapısı = URL yapısı.)

Temel Routing (Basic Routing)

app/
├── page.tsx              → /
├── about/page.tsx        → /about
├── contact/page.tsx      → /contact
└── blog/
    ├── page.tsx          → /blog
    └── archive/page.tsx  → /blog/archive

Dynamic Routes (Dinamik Rotalar)

tsx
// app/blog/[slug]/page.tsx → /blog/hello-world, /blog/nextjs-guide, etc.
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  return <article><h1>Post: {slug}</h1></article>;
}
tsx
// app/shop/[...categories]/page.tsx → catch-all route
// /shop/clothes, /shop/clothes/tops, /shop/clothes/tops/t-shirts
type Props = {
  params: Promise<{ categories: string[] }>;
};

export default async function ShopCategory({ params }: Props) {
  const { categories } = await params;
  // categories = ["clothes", "tops", "t-shirts"]
  return <div>Kategori: {categories.join(" > ")}</div>;
}
tsx
// app/shop/[[...categories]]/page.tsx → optional catch-all
// /shop da eşleşir (categories = undefined)

Route Groups (Rota Grupları)

Parantezli klasörler URL'ye yansımaz — layout paylaşımı için kullanılır.

app/
├── (marketing)/
│   ├── layout.tsx        # Marketing layout
│   ├── about/page.tsx    → /about
│   └── blog/page.tsx     → /blog
├── (dashboard)/
│   ├── layout.tsx        # Dashboard layout (farklı layout)
│   ├── settings/page.tsx → /settings
│   └── profile/page.tsx  → /profile

Route group'lar ayni seviyede birden fazla root layout tanimlamaya da olanak tanir. Ornegin (marketing) ve (dashboard) için tamamen farkli layout.tsx dosyaları kullanilabilir; bu sayede farkli bolumler için farkli navigasyon, footer ve tema uygulanabilir.

Parallel Routes (Paralel Rotalar)

Aynı layout içinde birden fazla sayfayı aynı anda göstermek için kullanılır.

tsx
// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      {children}
      <div className="grid grid-cols-2">
        {analytics}
        {team}
      </div>
    </div>
  );
}

// app/@analytics/page.tsx  → analytics slot
// app/@team/page.tsx       → team slot

Parallel route'larda bir slot'un aktif rotasi yoksa default.tsx dosyasi fallback olarak gosterilir. Bu dosya tanimlanmazsa Next.js 404 dondurur. Her slot için default.tsx tanimlamak iyi bir pratiktir.

tsx
// app/@analytics/default.tsx
export default function AnalyticsDefault() {
  return null; // veya bir placeholder
}

Intercepting Routes (Yakalayıcı Rotalar)

Modal gibi davranışlar için — bir rota başka bir rotayı "yakalar".

app/
├── feed/
│   └── page.tsx
├── photo/[id]/
│   └── page.tsx            # Tam sayfa görünümü
└── @modal/
    └── (.)photo/[id]/
        └── page.tsx        # Modal olarak gösterir

Convention: (.) same level, (..) one level up, (...) root level.

tsx
import Link from "next/link";
import { useRouter } from "next/navigation"; // App Router

// Deklaratif navigasyon
<Link href="/about">Hakkında</Link>
<Link href={`/blog/${slug}`}>Yazıyı Oku</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>

// Programatik navigasyon (Client Component içinde)
"use client";
import { useRouter } from "next/navigation";

function MyComponent() {
  const router = useRouter();

  function handleClick() {
    router.push("/dashboard");
    router.replace("/login");  // Geçmişe eklenmez
    router.back();             // Geri git
    router.refresh();          // Sayfayı yenile (cache temizle)
  }
}

4) App Router Derinlemesine

Server Components vs Client Components

App Router'da tüm bileşenleri varsayılan olarak Server Component'tir. Sunucuda render edilir, client'a JavaScript gonderilmez.

ÖzellikServer ComponentClient Component
VarsayılanEvetHayir ("use client" gerekir)
async/awaitDesteklenirDesteklenmez
Veritabani erisimiDogrudanAPI uzerinden
useState/useEffectKullanilamazKullanilir
Bundle boyutu0 KBEklenir
Hassas veri (API key)GuvenliSizabilir

Layouts vs Templates

layout.tsx ve template.tsx arasindaki temel fark: layout state'i korur ve navigasyonlarda yeniden render edilmez; template ise her navigasyonda sifirdan mount edilir.

tsx
// app/dashboard/layout.tsx — state korunur
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav>Dashboard Navigasyon</nav>
      <main>{children}</main>
    </div>
  );
}
tsx
// app/dashboard/template.tsx — her navigasyonda yeniden render
// Ornegin giris animasyonlari veya sayfa goruntulenme sayaci icin
export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="animate-fadeIn">{children}</div>;
}

Loading ve Error Dosyalarinin Calismasi

loading.tsx dosyasi React Suspense boundary olusturur. page.tsx icindeki async islemler tamamlanana kadar otomatik olarak gosterilir. Her rota segmentine ayri loading.tsx yerlestirilebilir.

error.tsx dosyasi React Error Boundary olusturur. Ilgili segment ve alt segmentlerindeki hataları yakalar. "use client" direktifi zorunludur cunku error boundary client-side bir React ozelligi.

global-error.tsx ise root layout'taki hataları yakalar. Root layout'un kendisini de sarmalar ve <html>, <body> etiketlerini icermelidir.


5) Rendering Stratejileri

Next.js 4 farklı rendering stratejisi sunar. Doğru seçim performansı doğrudan etkiler.

SSR — Server-Side Rendering (Sunucu Taraflı Render)

Her istekte sunucuda render edilir. Dinamik, her zaman güncel veri.

tsx
// Her istekte çalışır — cache yok
async function getLatestPosts() {
  const res = await fetch("https://api.example.com/posts", {
    cache: "no-store", // SSR: her istekte taze veri
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getLatestPosts();
  return (
    <ul>
      {posts.map((post: { id: number; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

SSG — Static Site Generation (Statik Site Oluşturma)

Build zamanında HTML oluşturulur. En hızlı yöntem.

tsx
// Build zamanında oluşturulur, sonraki isteklerde cache'ten döner
async function getProducts() {
  const res = await fetch("https://api.example.com/products", {
    cache: "force-cache", // SSG: build zamanında cache'le
  });
  return res.json();
}

// Dinamik rotalar için hangi sayfaların build zamanında oluşturulacağını belirle
export async function generateStaticParams() {
  const products = await getProducts();
  return products.map((product: { slug: string }) => ({
    slug: product.slug,
  }));
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // ...
}

ISR — Incremental Static Regeneration (Artımlı Statik Yenileme)

Statik sayfa belirli aralıklarla yeniden oluşturulur. SSG + SSR arası bir çözüm.

tsx
async function getNews() {
  const res = await fetch("https://api.example.com/news", {
    next: { revalidate: 3600 }, // ISR: 1 saat sonra yeniden oluştur
  });
  return res.json();
}

// Alternatif: sayfa düzeyinde revalidate
export const revalidate = 3600; // saniye cinsinden

CSR — Client-Side Rendering (İstemci Taraflı Render)

Tarayıcıda render edilir — interaktif bileşenler için.

tsx
"use client";

import { useState, useEffect } from "react";

export default function Dashboard() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/dashboard")
      .then((res) => res.json())
      .then(setData);
  }, []);

  if (!data) return <p>Yükleniyor...</p>;
  return <div>{/* dashboard UI */}</div>;
}

Hangi Strateji Ne Zaman? (Karar Agaci)

StratejiKullanım AlanıÖrnekVeri TazeligiPerformans
SSGNadiren değişen içerikBlog yazıları, dokümantasyonBuild zamaniEn hızlı
ISRPeriyodik güncellenen içerikHaber sitesi, ürün listesiBelirli araliklarlaHızlı
SSRHer istekte farklı veriKullanıcı dashboard'u, arama sonuçlarıHer istekteOrta
CSRYoğun etkileşim, SEO gereksizAdmin panel, form wizardClient'taDeğişken

6) Server Components vs Client Components

App Router'da bileşenler varsayılan olarak Server Component'tir.
(Components are Server Components by default.)

Server Components (Sunucu Bileşenleri)

tsx
// app/users/page.tsx — Server Component (varsayılan)
// Doğrudan veritabanına erişebilir, API key'leri güvenle kullanılır
import { db } from "@/lib/db";

export default async function UsersPage() {
  const users = await db.user.findMany(); // Doğrudan DB sorgusu!

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Avantajları (Benefits):

  • Sıfır JavaScript client'a gönderilir (zero client JS)
  • Doğrudan DB/API erişimi (direct database access)
  • API key'ler güvende kalır (secrets stay on server)
  • Daha küçük bundle size

Client Components (İstemci Bileşenleri)

tsx
// components/counter.tsx — Client Component
"use client"; // Bu direktif zorunlu!

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Sayaç: {count}
    </button>
  );
}

Ne Zaman Hangisi? (Decision Guide)

İhtiyaçServerClient
Data fetch (veri çekme)
Backend kaynakları (DB, API keys)
useState, useEffect
onClick, onChange (event handlers)
Browser API (localStorage, window)
Ağır kütüphaneler (heavy dependencies)✅ tercih etgerekirse

Composition Pattern (Bileşim Deseni)

Server Component'ler Client Component'leri sarar — tersi mümkün değildir (doğrudan).

tsx
// app/dashboard/page.tsx — Server Component
import { db } from "@/lib/db";
import Chart from "@/components/chart"; // Client Component

export default async function DashboardPage() {
  const data = await db.analytics.getWeekly(); // Sunucuda veri çek

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Veriyi Client Component'e prop olarak geçir */}
      <Chart data={data} />
    </div>
  );
}
tsx
// components/chart.tsx
"use client";

import { useState } from "react";

type ChartProps = {
  data: { label: string; value: number }[];
};

export default function Chart({ data }: ChartProps) {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
  // Interaktif chart render...
  return <div>{/* chart */}</div>;
}

Best Practice: "use client" sınırını mümkün olduğunca aşağıda tut.
Tüm sayfayı Client Component yapma — sadece interaktif parçaları ayır.


7) Data Fetching

App Router'da veri çekme doğrudan async Server Component'ler ile yapılır.

fetch API (Server Components)

tsx
// Next.js fetch'i genişletir — cache ve revalidation desteği ekler
async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    // Cache seçenekleri:
    cache: "force-cache",           // SSG — varsayılan (Next.js 14)
    // cache: "no-store",           // SSR — her istekte taze
    // next: { revalidate: 60 },    // ISR — 60 saniyede bir yenile
    // next: { tags: ["posts"] },   // On-demand revalidation tag
  });

  if (!res.ok) throw new Error("Veri çekilemedi");
  return res.json();
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);

  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

Parallel Data Fetching (Paralel Veri Çekme)

tsx
// İki bağımsız isteği paralel çalıştır — sıralı bekleme yapma!
export default async function Dashboard() {
  // Yanlış: sıralı (sequential) — yavaş
  // const user = await getUser();
  // const posts = await getPosts();

  // Doğru: paralel — hızlı
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts(),
  ]);

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
    </div>
  );
}

generateStaticParams ile Statik Sayfa Oluşturma

Dinamik rotalarin build zamaninda hangi parametrelerle olusturulacagini belirler. SSG ile birlikte kullanilir.

tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((r) =>
    r.json()
  );

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

// dynamicParams = false yapilirsa tanimlanmayan slug'lar 404 doner
export const dynamicParams = true; // varsayilan: true (ISR ile yeni sayfalar olusturulur)

Streaming ve Suspense

Server Component'lerde uzun suren veri cekme islemleri için Suspense kullanarak sayfanin hazir olan kisimlarini once gosterebilirsiniz.

tsx
import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Hizli icerik hemen gosterilir */}
      <Suspense fallback={<p>Istatistikler yukleniyor...</p>}>
        <SlowStats />
      </Suspense>

      <Suspense fallback={<p>Son siparisler yukleniyor...</p>}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

// Her biri bagimsiz olarak stream edilir
async function SlowStats() {
  const stats = await getStats(); // 2 saniye surebilir
  return <div>{stats.totalUsers} kullanici</div>;
}

async function RecentOrders() {
  const orders = await getOrders(); // 3 saniye surebilir
  return <ul>{orders.map((o: any) => <li key={o.id}>{o.name}</li>)}</ul>;
}

On-Demand Revalidation (İsteğe Bağlı Yenileme)

typescript
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Yetkisiz" }, { status: 401 });
  }

  // Tag ile yenile
  revalidateTag(tag);

  // Veya path ile yenile
  // revalidatePath("/blog");

  return NextResponse.json({ revalidated: true, now: Date.now() });
}

unstable_cache (Sunucu Cache)

typescript
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";

const getCachedUser = unstable_cache(
  async (userId: string) => {
    return db.user.findUnique({ where: { id: userId } });
  },
  ["user-cache"],               // cache key
  { revalidate: 3600, tags: ["users"] } // 1 saat cache
);

8) Server Actions

Server Actions, form gönderimi ve veri mutasyonu için sunucuda çalışan fonksiyonlardır.
(Server-side functions for form submissions and data mutations.)

Temel Kullanım (Basic Usage)

tsx
// app/contact/page.tsx
export default function ContactPage() {
  async function submitForm(formData: FormData) {
    "use server"; // Bu fonksiyon sunucuda çalışır!

    const name = formData.get("name") as string;
    const email = formData.get("email") as string;

    // Doğrudan DB'ye yaz
    await db.contact.create({ data: { name, email } });

    // Cache'i temizle
    revalidatePath("/contacts");
  }

  return (
    <form action={submitForm}>
      <input name="name" placeholder="İsim" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Gönder</button>
    </form>
  );
}

Ayrı Dosyada Server Actions

typescript
// lib/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";

const CreatePostSchema = z.object({
  title: z.string().min(3, "Başlık en az 3 karakter olmalı"),
  content: z.string().min(10),
});

type ActionState = {
  errors?: Record<string, string[]>;
  message?: string;
};

export async function createPost(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  const parsed = CreatePostSchema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  try {
    await db.post.create({ data: parsed.data });
  } catch (e) {
    return { message: "Veritabanı hatası" };
  }

  revalidatePath("/blog");
  redirect("/blog");
}

useActionState ile Form (React 19)

tsx
"use client";

import { useActionState } from "react";
import { createPost } from "@/lib/actions";

export default function PostForm() {
  const [state, formAction, isPending] = useActionState(createPost, {});

  return (
    <form action={formAction}>
      <input name="title" placeholder="Başlık" />
      {state.errors?.title && (
        <p className="text-red-500">{state.errors.title[0]}</p>
      )}

      <textarea name="content" placeholder="İçerik" />
      {state.errors?.content && (
        <p className="text-red-500">{state.errors.content[0]}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? "Kaydediliyor..." : "Kaydet"}
      </button>

      {state.message && <p className="text-red-500">{state.message}</p>}
    </form>
  );
}

Optimistic UI ile Server Actions

useOptimistic hook'u ile sunucu yaniti beklenmeden arayuz aninda guncellenebilir.

tsx
"use client";

import { useOptimistic } from "react";
import { addTodo } from "@/lib/actions";

type Todo = { id: string; title: string };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state: Todo[], newTodo: Todo) => [...state, newTodo]
  );

  async function handleSubmit(formData: FormData) {
    const title = formData.get("title") as string;

    // Arayuzu aninda guncelle
    addOptimisticTodo({ id: "temp-" + Date.now(), title });

    // Sunucuya gonder
    await addTodo(formData);
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="title" placeholder="Yeni gorev" />
        <button type="submit">Ekle</button>
      </form>
    </div>
  );
}

revalidatePath vs revalidateTag

typescript
"use server";

import { revalidatePath, revalidateTag } from "next/cache";

export async function updatePost(id: string, data: any) {
  await db.post.update({ where: { id }, data });

  // Path bazli: belirli bir sayfanin cache'ini temizle
  revalidatePath("/blog");
  revalidatePath(`/blog/${id}`);

  // Tag bazli: fetch sirasinda tanimlanan tag'e gore temizle
  // fetch(..., { next: { tags: ["posts"] } })
  revalidateTag("posts");
}

Mutation Patterns (Mutasyon Desenleri)

tsx
"use client";

import { useTransition } from "react";
import { deletePost } from "@/lib/actions";

export function DeleteButton({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() =>
        startTransition(async () => {
          await deletePost(postId);
        })
      }
    >
      {isPending ? "Siliniyor..." : "Sil"}
    </button>
  );
}

9) API Routes

App Router'da API route'ları route.ts dosyaları ile oluşturulur.

Temel API Route

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

// GET /api/users
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get("page") ?? "1");
  const limit = parseInt(searchParams.get("limit") ?? "10");

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ users, page, limit });
}

// POST /api/users
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const user = await db.user.create({ data: body });
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "Kullanıcı oluşturulamadı" },
      { status: 400 }
    );
  }
}

Dinamik API Route

typescript
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";

type RouteContext = {
  params: Promise<{ id: string }>;
};

// GET /api/users/123
export async function GET(request: NextRequest, { params }: RouteContext) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });

  if (!user) {
    return NextResponse.json({ error: "Bulunamadı" }, { status: 404 });
  }

  return NextResponse.json(user);
}

// PUT /api/users/123
export async function PUT(request: NextRequest, { params }: RouteContext) {
  const { id } = await params;
  const body = await request.json();
  const user = await db.user.update({ where: { id }, data: body });
  return NextResponse.json(user);
}

// DELETE /api/users/123
export async function DELETE(request: NextRequest, { params }: RouteContext) {
  const { id } = await params;
  await db.user.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

Response Helpers

typescript
// Headers ayarlama
export async function GET() {
  return NextResponse.json(
    { data: "value" },
    {
      status: 200,
      headers: {
        "Cache-Control": "public, max-age=3600",
        "X-Custom-Header": "value",
      },
    }
  );
}

// Cookies
import { cookies } from "next/headers";

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get("token")?.value;

  const response = NextResponse.json({ authenticated: !!token });
  response.cookies.set("visited", "true", { maxAge: 86400 });
  return response;
}

// Redirect
import { redirect } from "next/navigation";

export async function GET() {
  redirect("/login"); // 307 redirect
}

10) Middleware

Middleware, her istekten önce çalışan sunucu taraflı koddur.
(Server-side code that runs before every matching request.)

typescript
// middleware.ts — proje kök dizinine yerleştirilir (src/ kullanıyorsan src/middleware.ts)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1) Authentication kontrolü
  const token = request.cookies.get("auth-token")?.value;

  if (pathname.startsWith("/dashboard") && !token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 2) Locale redirect
  if (pathname === "/") {
    const locale = request.headers.get("accept-language")?.startsWith("tr")
      ? "tr"
      : "en";
    return NextResponse.redirect(new URL(`/${locale}`, request.url));
  }

  // 3) Header ekleme
  const response = NextResponse.next();
  response.headers.set("x-pathname", pathname);
  return response;
}

// Matcher: hangi path'lerde çalışacağını belirle
export const config = {
  matcher: [
    // Statik dosyalar ve API dışında her şey
    "/((?!_next/static|_next/image|favicon.ico|api).*)",
    // Veya spesifik path'ler
    // "/dashboard/:path*",
    // "/admin/:path*",
  ],
};

Rewrite Örneği (URL Masking)

typescript
export function middleware(request: NextRequest) {
  // /old-blog/post → /blog/post (URL değişmez ama içerik farklı)
  if (request.nextUrl.pathname.startsWith("/old-blog")) {
    return NextResponse.rewrite(
      new URL(
        request.nextUrl.pathname.replace("/old-blog", "/blog"),
        request.url
      )
    );
  }
}

Rate Limiting (Basit)

typescript
const rateLimit = new Map<string, { count: number; timestamp: number }>();

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/api")) {
    const ip = request.headers.get("x-forwarded-for") ?? "unknown";
    const now = Date.now();
    const windowMs = 60_000; // 1 dakika
    const maxRequests = 100;

    const record = rateLimit.get(ip);
    if (record && now - record.timestamp < windowMs) {
      if (record.count >= maxRequests) {
        return NextResponse.json(
          { error: "Çok fazla istek" },
          { status: 429 }
        );
      }
      record.count++;
    } else {
      rateLimit.set(ip, { count: 1, timestamp: now });
    }
  }

  return NextResponse.next();
}

11) Prisma Entegrasyonu

Kurulum ve Yapılandırma

bash
npm install prisma @prisma/client
npx prisma init

Schema Tanimlama

prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
}

Migration

bash
# Migration olustur ve uygula
npx prisma migrate dev --name init

# Production'da migration uygula
npx prisma migrate deploy

# Prisma Studio ile veritabanini gorsel olarak incele
npx prisma studio

Singleton Pattern (Önemli)

Next.js development modunda hot reload nedeniyle birden fazla Prisma Client instance'i olusabilir. Bunu onlemek için singleton pattern kullanilmalidir.

typescript
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

CRUD Ornekleri

typescript
// lib/db/users.ts
import { prisma } from "@/lib/prisma";

// Create
export async function createUser(email: string, name: string) {
  return prisma.user.create({
    data: { email, name },
  });
}

// Read
export async function getUsers() {
  return prisma.user.findMany({
    include: { posts: true },
    orderBy: { createdAt: "desc" },
  });
}

export async function getUserById(id: string) {
  return prisma.user.findUnique({
    where: { id },
    include: { posts: { where: { published: true } } },
  });
}

// Update
export async function updateUser(id: string, data: { name?: string; email?: string }) {
  return prisma.user.update({
    where: { id },
    data,
  });
}

// Delete
export async function deleteUser(id: string) {
  return prisma.user.delete({ where: { id } });
}

12) Styling

CSS Modules

tsx
// components/button.module.css
// .primary { background: blue; color: white; padding: 8px 16px; }
// .danger { background: red; color: white; }

// components/button.tsx
import styles from "./button.module.css";

export function Button({ variant = "primary" }: { variant?: "primary" | "danger" }) {
  return (
    <button className={styles[variant]}>
      Tıkla
    </button>
  );
}

Tailwind CSS (Önerilen)

tsx
// Tailwind zaten create-next-app ile kurulabilir
// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        brand: {
          primary: "#6366f1",
          secondary: "#8b5cf6",
        },
      },
    },
  },
  plugins: [],
};

export default config;
tsx
// Tailwind ile bileşen
export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm
                    hover:shadow-md transition-shadow dark:bg-gray-800 dark:border-gray-700">
      <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
        {title}
      </h3>
      <div className="mt-2 text-gray-600 dark:text-gray-300">
        {children}
      </div>
    </div>
  );
}

styled-components (App Router ile)

tsx
// lib/styled-registry.tsx — App Router ile styled-components kullanmak için gerekli
"use client";

import React, { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== "undefined") return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}
tsx
// app/layout.tsx — Registry'yi layout'a ekle
import StyledComponentsRegistry from "@/lib/styled-registry";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="tr">
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

13) Image & Font Optimization

next/image

tsx
import Image from "next/image";

// Lokal resim (otomatik boyut)
import heroImage from "@/public/hero.jpg";

export function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Hero banner"
      placeholder="blur"       // Yüklenirken bulanık placeholder
      priority                 // LCP için öncelikli yükle
      className="rounded-lg"
    />
  );
}

// Uzak resim (boyut zorunlu)
export function Avatar({ src, name }: { src: string; name: string }) {
  return (
    <Image
      src={src}
      alt={name}
      width={48}
      height={48}
      className="rounded-full"
    />
  );
}

// Fill mode — parent container'ı doldurur
export function BackgroundImage() {
  return (
    <div className="relative h-64 w-full">
      <Image
        src="/background.jpg"
        alt="Background"
        fill
        className="object-cover"
        sizes="100vw"
      />
    </div>
  );
}

sizes prop'u responsive tasarimlarda onemlidir. Tarayiciya hangi viewport genisliginde hangi boyutta resim yuklemesi gerektigini bildirir. Ornegin: sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw".

typescript
// next.config.ts — uzak resimlere izin ver
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "avatars.githubusercontent.com",
      },
      {
        protocol: "https",
        hostname: "**.cloudinary.com",
      },
    ],
  },
};

export default nextConfig;

next/font

tsx
// app/layout.tsx — Google Fonts (sıfır layout shift, self-hosted)
import { Inter, JetBrains_Mono } from "next/font/google";

const inter = Inter({
  subsets: ["latin", "latin-ext"],
  display: "swap",
  variable: "--font-inter",
});

const jetbrainsMono = JetBrains_Mono({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-mono",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="tr" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}
tsx
// Lokal font
import localFont from "next/font/local";

const myFont = localFont({
  src: [
    { path: "../fonts/MyFont-Regular.woff2", weight: "400", style: "normal" },
    { path: "../fonts/MyFont-Bold.woff2", weight: "700", style: "normal" },
  ],
  variable: "--font-custom",
});

14) Authentication

NextAuth.js (Auth.js v5) Kurulumu

bash
npm install next-auth@beta
typescript
// auth.ts — proje kök dizini
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),
    Credentials({
      name: "Email & Password",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Şifre", type: "password" },
      },
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user?.hashedPassword) return null;

        const isValid = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        );
        return isValid ? user : null;
      },
    }),
  ],
  pages: {
    signIn: "/login",
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isProtected = nextUrl.pathname.startsWith("/dashboard");
      if (isProtected && !isLoggedIn) {
        return Response.redirect(new URL("/login", nextUrl));
      }
      return true;
    },
  },
});
typescript
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

Middleware ile Route Korumasi

Auth.js v5'te middleware dogrudan entegre çalışır. authorized callback'i middleware icerisinde otomatik olarak tetiklenir.

typescript
// middleware.ts
import { auth } from "@/auth";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
  const isAuthPage = req.nextUrl.pathname.startsWith("/login");

  // Giris yapmis kullanici login sayfasina erismeye calisirsa
  if (isAuthPage && isLoggedIn) {
    return Response.redirect(new URL("/dashboard", req.nextUrl));
  }

  // Korunmus sayfalara giris yapmadan erismek
  if (isProtected && !isLoggedIn) {
    const loginUrl = new URL("/login", req.nextUrl);
    loginUrl.searchParams.set("callbackUrl", req.nextUrl.pathname);
    return Response.redirect(loginUrl);
  }
});

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Session Kullanımı

tsx
// Server Component'te session
import { auth } from "@/auth";

export default async function ProfilePage() {
  const session = await auth();

  if (!session?.user) {
    redirect("/login");
  }

  return <h1>Hoş geldin, {session.user.name}</h1>;
}
tsx
// Client Component'te session
"use client";

import { useSession } from "next-auth/react";

export function UserMenu() {
  const { data: session, status } = useSession();

  if (status === "loading") return <p>Yükleniyor...</p>;
  if (!session) return <a href="/login">Giriş Yap</a>;

  return <p>{session.user?.name}</p>;
}

Protected Route Pattern

tsx
// components/auth-guard.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function AuthGuard({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();
  if (!session) redirect("/login");
  return <>{children}</>;
}

// app/dashboard/layout.tsx
import AuthGuard from "@/components/auth-guard";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <AuthGuard>{children}</AuthGuard>;
}

15) Güvenlik

Server Actions'da Zod Validation

Server Actions disaridan erisilebilir HTTP endpoint'leri gibi davranir. Bu nedenle her zaman girdi dogrulamasi yapilmalidir.

typescript
"use server";

import { z } from "zod";

const ContactSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  message: z.string().min(10).max(1000),
});

export async function submitContact(formData: FormData) {
  const parsed = ContactSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  });

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }

  // Guvenli veri ile devam et
  await db.contact.create({ data: parsed.data });
}

CSRF Korumasi

Next.js Server Actions otomatik olarak CSRF token uretir ve dogrular. Ek bir işlem gerekmez. Ancak ozel API route'lari için dikkatli olunmalidir.

typescript
// API route'larinda Origin header kontrolu
export async function POST(request: NextRequest) {
  const origin = request.headers.get("origin");
  const allowedOrigins = [process.env.NEXT_PUBLIC_APP_URL];

  if (!origin || !allowedOrigins.includes(origin)) {
    return NextResponse.json({ error: "Yetkisiz" }, { status: 403 });
  }

  // ...
}

NEXT_PUBLIC_ Env Ayrimi

NEXT_PUBLIC_ on eki ile baslayan ortam degiskenleri client-side JavaScript bundle'ina dahil edilir. Hassas bilgileri asla bu on ek ile tanimlamayin.

bash
# Guvenli — sadece sunucuda erisilebilir
DATABASE_URL="postgresql://..."
API_SECRET="gizli-anahtar"
STRIPE_SECRET_KEY="sk_..."

# Tehlikeli olabilir — client'a gonderilir
NEXT_PUBLIC_APP_URL="https://example.com"
NEXT_PUBLIC_ANALYTICS_ID="GA-123"
# ASLA: NEXT_PUBLIC_API_SECRET="..." yapmayın!

Rate Limiting (Üretim Ortami)

Basit in-memory rate limiting yerine üretim ortaminda Redis tabanli cozumler kullanilmalidir.

typescript
// Upstash Redis ile rate limiting ornegi
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"), // 10 saniyede 10 istek
});

export async function POST(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: "Cok fazla istek, lutfen bekleyin" },
      { status: 429 }
    );
  }

  // Normal islem...
}

16) Metadata & SEO

Static Metadata

tsx
// app/layout.tsx veya herhangi bir page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    default: "My App",
    template: "%s | My App", // Alt sayfalar: "Hakkında | My App"
  },
  description: "Next.js ile geliştirilmiş modern web uygulaması",
  keywords: ["nextjs", "react", "typescript"],
  authors: [{ name: "Fahri" }],
  openGraph: {
    title: "My App",
    description: "Next.js ile geliştirilmiş modern web uygulaması",
    url: "https://example.com",
    siteName: "My App",
    images: [
      {
        url: "/og-image.png",
        width: 1200,
        height: 630,
        alt: "My App OG Image",
      },
    ],
    locale: "tr_TR",
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "My App",
    description: "Next.js ile geliştirilmiş modern web uygulaması",
    images: ["/og-image.png"],
  },
  robots: {
    index: true,
    follow: true,
  },
};

Dynamic Metadata (generateMetadata)

tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{/* ... */}</article>;
}

Sitemap & Robots

typescript
// app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();

  const blogUrls = posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    {
      url: "https://example.com",
      lastModified: new Date(),
      changeFrequency: "yearly",
      priority: 1,
    },
    {
      url: "https://example.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.5,
    },
    ...blogUrls,
  ];
}
typescript
// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/api/", "/admin/"],
    },
    sitemap: "https://example.com/sitemap.xml",
  };
}

JSON-LD (Structured Data)

tsx
// components/json-ld.tsx
export function ArticleJsonLd({
  title,
  description,
  publishedTime,
  author,
  url,
}: {
  title: string;
  description: string;
  publishedTime: string;
  author: string;
  url: string;
}) {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: title,
    description,
    datePublished: publishedTime,
    author: { "@type": "Person", name: author },
    url,
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

17) Test

Vitest ile Component Testi

bash
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdom
typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./vitest.setup.ts"],
    include: ["**/*.test.{ts,tsx}"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
typescript
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
tsx
// components/counter.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Counter from "./counter";

describe("Counter", () => {
  it("baslangic degeri 0 olmali", () => {
    render(<Counter />);
    expect(screen.getByText(/sayac: 0/i)).toBeInTheDocument();
  });

  it("tiklandiginda artmali", () => {
    render(<Counter />);
    fireEvent.click(screen.getByRole("button"));
    expect(screen.getByText(/sayac: 1/i)).toBeInTheDocument();
  });
});

Playwright ile E2E Test

bash
npm install -D @playwright/test
npx playwright install
typescript
// e2e/home.spec.ts
import { test, expect } from "@playwright/test";

test("ana sayfa yuklenmeli", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(/my app/i);
  await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});

test("blog sayfasina navigasyon", async ({ page }) => {
  await page.goto("/");
  await page.click('a[href="/blog"]');
  await expect(page).toHaveURL(/\/blog/);
  await expect(page.getByRole("heading")).toContainText("Blog");
});

test("iletisim formu gonderilebilmeli", async ({ page }) => {
  await page.goto("/contact");
  await page.fill('input[name="name"]', "Test Kullanici");
  await page.fill('input[name="email"]', "test@example.com");
  await page.click('button[type="submit"]');
  await expect(page.getByText("Basariyla gonderildi")).toBeVisible();
});

MSW ile API Mocking

bash
npm install -D msw
typescript
// mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users", () => {
    return HttpResponse.json([
      { id: "1", name: "Ali", email: "ali@example.com" },
      { id: "2", name: "Veli", email: "veli@example.com" },
    ]);
  }),

  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: "3", ...body },
      { status: 201 }
    );
  }),
];
typescript
// mocks/server.ts — test ortami icin
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
typescript
// vitest.setup.ts icine ekle
import { server } from "./mocks/server";
import { beforeAll, afterEach, afterAll } from "vitest";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

18) Deployment

Vercel (Önerilen — En Kolay)

bash
# Vercel CLI ile deploy
npm i -g vercel
vercel                  # Preview deployment
vercel --prod           # Production deployment

# Veya GitHub repo'yu Vercel dashboard'dan bağla
# Her push otomatik deploy olur

Self-Hosted (Node.js)

bash
# Build
npm run build

# Start (production)
npm start
# veya
node .next/standalone/server.js  # standalone output ile
typescript
// next.config.ts — standalone output
const nextConfig: NextConfig = {
  output: "standalone", // Docker için ideal
};

Docker

dockerfile
# Dockerfile
FROM node:20-alpine AS base

# Dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]
bash
docker build -t my-next-app .
docker run -p 3000:3000 my-next-app

Environment Variables (Ortam Değişkenleri)

bash
# .env.local — git'e eklenmez, sadece lokal geliştirme
DATABASE_URL="postgresql://..."
API_SECRET="gizli-anahtar"

# NEXT_PUBLIC_ prefix'i client tarafında erişilebilir yapar
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_ANALYTICS_ID="GA-123"
typescript
// Server-side (sunucu tarafı) — tüm env erişilebilir
const dbUrl = process.env.DATABASE_URL;

// Client-side (istemci tarafı) — sadece NEXT_PUBLIC_ erişilebilir
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
DosyaOrtamGit'e eklenir?
.envTüm ortamlarEvet
.env.localLokal geliştirmeHayır
.env.developmentnext devEvet
.env.productionnext build & next startEvet

19) Tips ve En Iyi Pratikler

SSR vs SSG vs ISR Karar Agaci

SoruEvet iseHayir ise
Veri hic degisiyor mu?SSGSSG
Veri saatte bir degisiyor mu?ISR (revalidate)Sonraki soruya gec
Her kullanici farkli içerik goruyor mu?SSRISR veya SSG
SEO gerekli mi?SSR veya SSG/ISRCSR yeterli
Sayfa cok interaktif mi?CSR + SSR hybridSSG/ISR

next/image Performans İpuçları

  • LCP (Largest Contentful Paint) olan resimlere priority ekleyin
  • sizes prop'unu her zaman belirtin — gereksiz büyük resim indirmeyi onler
  • Uzak resimler için remotePatterns kullanin, domains yerine (daha güvenli)
  • Placeholder olarak blur kullanin — kullanici deneyimini iyilestirir

Bundle Analysis

bash
# Bundle boyutunu analiz etmek icin
npm install -D @next/bundle-analyzer

# next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";

const nextConfig = withBundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
})({
  // diger yapilandirmalar
});

export default nextConfig;

# Calistirma
ANALYZE=true npm run build

Metadata API Ozet

tsx
// Statik metadata — page.tsx veya layout.tsx icinde export
export const metadata: Metadata = { title: "Sayfa Basligi" };

// Dinamik metadata — async fonksiyon
export async function generateMetadata({ params }): Promise<Metadata> { ... }

// title template — layout'ta tanimla, alt sayfalarda otomatik uygulanir
// layout: title: { template: "%s | Site Adi" }
// page: title: "Hakkinda"  → "Hakkinda | Site Adi"

Turbopack

Next.js 15 ile birlikte Turbopack development modunda varsayılan olarak kullanilabilir. Webpack'e gore önemli olcude hizlidir.

bash
# Turbopack ile gelistirme sunucusu
npx next dev --turbopack

# next.config.ts icinde de aktif edilebilir
# Not: Turbopack henuz tum webpack plugin'lerini desteklemez

20) Hızlı Referans

Dosya Kuralları Tablosu (File Conventions)

DosyaAmaçAçıklama
page.tsxSayfaBir rotayı erişilebilir yapar
layout.tsxYerleşimAlt sayfaları sarar, yeniden render edilmez
template.tsxŞablonLayout gibi ama her navigasyonda yeniden render
loading.tsxYüklenmeSuspense boundary, sayfa yüklenirken gösterilir
error.tsxHataError boundary, hata olunca gösterilir ("use client" zorunlu)
not-found.tsx404notFound() çağrıldığında gösterilir
global-error.tsxGlobal HataRoot layout hatalarını yakalar
route.tsAPIHTTP endpoint (GET, POST, PUT, DELETE)
default.tsxVarsayılanParallel route'larda fallback
middleware.tsMiddlewareİstek öncesi çalışır (proje kök dizininde)
opengraph-image.tsxOG ImageDinamik Open Graph resmi oluşturur
sitemap.tsSitemapXML sitemap oluşturur
robots.tsRobotsrobots.txt oluşturur
manifest.tsPWA ManifestWeb app manifest dosyası

next.config.ts Sık Kullanılan Ayarlar

typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Strict mode (React)
  reactStrictMode: true,

  // Image domains
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "**.example.com" },
    ],
  },

  // Redirect kuralları
  async redirects() {
    return [
      {
        source: "/old-page",
        destination: "/new-page",
        permanent: true, // 308
      },
    ];
  },

  // Rewrite kuralları
  async rewrites() {
    return [
      {
        source: "/api/proxy/:path*",
        destination: "https://external-api.com/:path*",
      },
    ];
  },

  // Headers
  async headers() {
    return [
      {
        source: "/api/:path*",
        headers: [
          { key: "Access-Control-Allow-Origin", value: "*" },
        ],
      },
    ];
  },

  // Standalone output (Docker için)
  output: "standalone",

  // TypeScript hataları build'i engellesin mi?
  typescript: {
    ignoreBuildErrors: false,
  },
};

export default nextConfig;

Sık Kullanılan Komutlar

bash
npx create-next-app@latest my-app   # Yeni proje oluştur
npm run dev                          # Geliştirme sunucusu (localhost:3000)
npm run build                        # Production build
npm start                            # Production sunucu
npm run lint                         # ESLint kontrolü

Import Cheat Sheet

typescript
// Navigation
import Link from "next/link";
import { redirect } from "next/navigation";          // Server
import { useRouter, usePathname, useSearchParams } from "next/navigation"; // Client

// Image & Font
import Image from "next/image";
import { Inter } from "next/font/google";
import localFont from "next/font/local";

// Server utilities
import { cookies, headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

// Cache
import { revalidatePath, revalidateTag } from "next/cache";
import { unstable_cache } from "next/cache";

// Metadata
import type { Metadata, MetadataRoute } from "next";

// Dynamic rendering
import { notFound } from "next/navigation";
import dynamic from "next/dynamic";

// Lazy load Client Component (kod bölme)
const HeavyComponent = dynamic(() => import("@/components/heavy"), {
  loading: () => <p>Yükleniyor...</p>,
  ssr: false, // Sadece client'ta render et
});

Sık Yapılan Hatalar (Common Mistakes)

HataÇözüm
Server Component'te useState kullanmak"use client" ekle veya bileşeni ayır
onClick Server Component'te çalışmıyorEvent handler'lar Client Component gerektirir
window is not defined hatası"use client" ekle veya typeof window !== "undefined" kontrolü
API key client'a sızıyorNEXT_PUBLIC_ prefix'ini kaldır, Server Component/API route kullan
params Promise olmadan kullanmakNext.js 15'te params ve searchParams Promise'dir, await et
Layout her navigasyonda yeniden render oluyorLayout değil template.tsx kullan veya state'i kontrol et
fetch cache beklendiği gibi çalışmıyorcache ve revalidate ayarlarını kontrol et
Prisma client coklu instance olusturuyorSingleton pattern kullan (lib/prisma.ts)
Server Action'da girdi dogrulanmiyorZod ile validation ekle, ham FormData'ya guvenme

Son not: Bu rehber Next.js 14/15 App Router odaklıdır.
Resmi dokümantasyon: nextjs.org/docs
Güncel kalması için Next.js sürüm notlarını takip edin.


Ilgili Rehberler

Frontend

Diger Kategoriler

Developer Guides & Technical References