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
| Feature | Pages Router (eski) | App Router (yeni, önerilen) |
|---|---|---|
| Directory | pages/ | app/ |
| Components | Client by default | Server by default |
| Layouts | _app.tsx, _document.tsx | layout.tsx (nested) |
| Data fetching | getServerSideProps, getStaticProps | async components, fetch |
| Streaming | Limited | Full support with Suspense |
| Server Actions | Not available | Built-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
# Ö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 devVisit: 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)
// 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>
);
}// 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>
);
}// 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>;
}// 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>
);
}// 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/archiveDynamic Routes (Dinamik Rotalar)
// 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>;
}// 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>;
}// 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 → /profileRoute 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.
// 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 slotParallel 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.
// 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österirConvention: (.) same level, (..) one level up, (...) root level.
Link & Navigation
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.
| Özellik | Server Component | Client Component |
|---|---|---|
| Varsayılan | Evet | Hayir ("use client" gerekir) |
| async/await | Desteklenir | Desteklenmez |
| Veritabani erisimi | Dogrudan | API uzerinden |
| useState/useEffect | Kullanilamaz | Kullanilir |
| Bundle boyutu | 0 KB | Eklenir |
| Hassas veri (API key) | Guvenli | Sizabilir |
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.
// app/dashboard/layout.tsx — state korunur
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav>Dashboard Navigasyon</nav>
<main>{children}</main>
</div>
);
}// 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.
// 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.
// 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.
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 cinsindenCSR — Client-Side Rendering (İstemci Taraflı Render)
Tarayıcıda render edilir — interaktif bileşenler için.
"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)
| Strateji | Kullanım Alanı | Örnek | Veri Tazeligi | Performans |
|---|---|---|---|---|
| SSG | Nadiren değişen içerik | Blog yazıları, dokümantasyon | Build zamani | En hızlı |
| ISR | Periyodik güncellenen içerik | Haber sitesi, ürün listesi | Belirli araliklarla | Hızlı |
| SSR | Her istekte farklı veri | Kullanıcı dashboard'u, arama sonuçları | Her istekte | Orta |
| CSR | Yoğun etkileşim, SEO gereksiz | Admin panel, form wizard | Client'ta | Değ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)
// 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)
// 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ç | Server | Client |
|---|---|---|
| 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 et | gerekirse |
Composition Pattern (Bileşim Deseni)
Server Component'ler Client Component'leri sarar — tersi mümkün değildir (doğrudan).
// 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>
);
}// 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)
// 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)
// İ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.
// 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.
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)
// 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)
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)
// 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
// 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)
"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.
"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
"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)
"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
// 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
// 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
// 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.)
// 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)
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)
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
npm install prisma @prisma/client
npx prisma initSchema Tanimlama
// 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
# 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 studioSingleton Pattern (Önemli)
Next.js development modunda hot reload nedeniyle birden fazla Prisma Client instance'i olusabilir. Bunu onlemek için singleton pattern kullanilmalidir.
// 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
// 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
// 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)
// 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;// 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)
// 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>
);
}// 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
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".
// 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
// 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>
);
}// 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
npm install next-auth@beta// 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;
},
},
});// 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.
// 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ı
// 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>;
}// 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
// 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.
"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.
// 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.
# 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.
// 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
// 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)
// 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
// 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,
];
}// 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)
// 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
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdom// 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"),
},
},
});// vitest.setup.ts
import "@testing-library/jest-dom/vitest";// 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
npm install -D @playwright/test
npx playwright install// 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
npm install -D msw// 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 }
);
}),
];// mocks/server.ts — test ortami icin
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);// 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)
# 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 olurSelf-Hosted (Node.js)
# Build
npm run build
# Start (production)
npm start
# veya
node .next/standalone/server.js # standalone output ile// next.config.ts — standalone output
const nextConfig: NextConfig = {
output: "standalone", // Docker için ideal
};Docker
# 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"]docker build -t my-next-app .
docker run -p 3000:3000 my-next-appEnvironment Variables (Ortam Değişkenleri)
# .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"// 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;| Dosya | Ortam | Git'e eklenir? |
|---|---|---|
.env | Tüm ortamlar | Evet |
.env.local | Lokal geliştirme | Hayır |
.env.development | next dev | Evet |
.env.production | next build & next start | Evet |
19) Tips ve En Iyi Pratikler
SSR vs SSG vs ISR Karar Agaci
| Soru | Evet ise | Hayir ise |
|---|---|---|
| Veri hic degisiyor mu? | SSG | SSG |
| Veri saatte bir degisiyor mu? | ISR (revalidate) | Sonraki soruya gec |
| Her kullanici farkli içerik goruyor mu? | SSR | ISR veya SSG |
| SEO gerekli mi? | SSR veya SSG/ISR | CSR yeterli |
| Sayfa cok interaktif mi? | CSR + SSR hybrid | SSG/ISR |
next/image Performans İpuçları
- LCP (Largest Contentful Paint) olan resimlere
priorityekleyin sizesprop'unu her zaman belirtin — gereksiz büyük resim indirmeyi onler- Uzak resimler için
remotePatternskullanin,domainsyerine (daha güvenli) - Placeholder olarak
blurkullanin — kullanici deneyimini iyilestirir
Bundle Analysis
# 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 buildMetadata API Ozet
// 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.
# Turbopack ile gelistirme sunucusu
npx next dev --turbopack
# next.config.ts icinde de aktif edilebilir
# Not: Turbopack henuz tum webpack plugin'lerini desteklemez20) Hızlı Referans
Dosya Kuralları Tablosu (File Conventions)
| Dosya | Amaç | Açıklama |
|---|---|---|
page.tsx | Sayfa | Bir rotayı erişilebilir yapar |
layout.tsx | Yerleşim | Alt sayfaları sarar, yeniden render edilmez |
template.tsx | Şablon | Layout gibi ama her navigasyonda yeniden render |
loading.tsx | Yüklenme | Suspense boundary, sayfa yüklenirken gösterilir |
error.tsx | Hata | Error boundary, hata olunca gösterilir ("use client" zorunlu) |
not-found.tsx | 404 | notFound() çağrıldığında gösterilir |
global-error.tsx | Global Hata | Root layout hatalarını yakalar |
route.ts | API | HTTP endpoint (GET, POST, PUT, DELETE) |
default.tsx | Varsayılan | Parallel route'larda fallback |
middleware.ts | Middleware | İstek öncesi çalışır (proje kök dizininde) |
opengraph-image.tsx | OG Image | Dinamik Open Graph resmi oluşturur |
sitemap.ts | Sitemap | XML sitemap oluşturur |
robots.ts | Robots | robots.txt oluşturur |
manifest.ts | PWA Manifest | Web app manifest dosyası |
next.config.ts Sık Kullanılan Ayarlar
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
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
// 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ıyor | Event handler'lar Client Component gerektirir |
window is not defined hatası | "use client" ekle veya typeof window !== "undefined" kontrolü |
| API key client'a sızıyor | NEXT_PUBLIC_ prefix'ini kaldır, Server Component/API route kullan |
params Promise olmadan kullanmak | Next.js 15'te params ve searchParams Promise'dir, await et |
| Layout her navigasyonda yeniden render oluyor | Layout değil template.tsx kullan veya state'i kontrol et |
fetch cache beklendiği gibi çalışmıyor | cache ve revalidate ayarlarını kontrol et |
| Prisma client coklu instance olusturuyor | Singleton pattern kullan (lib/prisma.ts) |
| Server Action'da girdi dogrulanmiyor | Zod 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
- Frontend Genel Bakış
- JavaScript & TypeScript
- TypeScript Rehberi
- React Rehberi
- Vue.js Rehberi
- CSS & Tailwind
- Web Performance
- Three.js Rehberi