Geçen ay bir arkadaşımın yan projesini inceliyordum. Üç hafta önce create-next-app ile başlamış, App Router seçmiş, sonra YouTube'da Pages Router daha kolay diyen bir video izleyince yarısını geri taşımış. Şimdi iki yapı bir arada, hangi sayfa nerede karışmış halde. Bu hikaye o kadar yaygın ki neredeyse bir geçiş ritüeli gibi.
Next.js 14 yeni başlayan biri için kafa karıştırıcı çünkü internet hâlâ 2022 cevaplarıyla dolu. App Router, Server Components, Server Actions, 'use client'... Bunların hepsi farklı zamanlarda farklı problemleri çözmek için geldi. Tek seferde hepsini öğrenmeye çalışırsanız boğulursunuz. Sırayla gidelim.
Next.js 14 aslında ne çözüyor?
İki şey: ilk yükleme hızı ve sunucu/istemci sınırının netleşmesi. Eski getServerSideProps dünyasında her sayfa ya tamamen sunucuda render ediliyordu ya tamamen tarayıcıda. Aralarında bir şey yoktu. Bir SaaS ekranını düşünün: sol tarafta proje listesi (sunucudan gelir, nadir değişir), ortada görev panosu (interaktif, sürükle bırak), sağda bildirimler (gerçek zamanlı). Eski mimaride bu üç parçayı tek bir sayfada verimli render etmek baş ağrısıydı.
App Router bu sınırı bileşen seviyesine indirdi. Aynı sayfada bir bileşen sunucuda çalışır, yanındaki tarayıcıda. JavaScript bundle'ı sadece gerçekten gerekli olan yerlere gönderilir. Poitim'in görev tahtasında, kart listesini sunucuda hazırlıyoruz; sürükleme mantığı sadece ihtiyacı olan iki bileşende client tarafında. Sonuç: ilk açılışta ekran neredeyse anında doluyor, JavaScript yükü çok küçük.
App Router mı, Pages Router mı?
Yeni başlıyorsanız, App Router. Tartışma kapalı. Pages Router hâlâ destekleniyor ama yeni özellikler oraya gelmiyor. Eski projelerinizi taşımak için acele etmeyin, ama yeni bir SaaS'a 2026'da Pages ile başlamak için artık geçerli bir sebep yok.
Pages Router'ın daha kolay göründüğü doğru. Çünkü zihinsel modeli daha basit: her dosya bir sayfa, herşey React'te alıştığımız gibi. App Router öğrenmesi için bir hafta verin kendinize. İlk üç gün öfkeleneceksiniz. Dördüncü gün tamam, neden böyle yaptıklarını anladım diyeceksiniz.
Server Components: en büyük zihinsel kırılma
App Router'da bir bileşen yazdığınızda varsayılan olarak Server Component'tir. Yani kodu sunucuda çalışır, tarayıcıya gönderilmez. useState, useEffect, onClick kullanamazsınız. İlk başta kısıtlayıcı geliyor, sonra fark ediyorsunuz: zaten gerek yokmuş.
// app/projects/page.tsx — Server Component
import { db } from '@/lib/db';
import ProjectCard from './project-card';
export default async function ProjectsPage() {
const projects = await db.project.findMany({
where: { archived: false },
orderBy: { updatedAt: 'desc' },
});
return (
{projects.map(p => )}
);
}Burada dikkat edin: useEffect yok, fetch yok, useState yok. Veritabanına doğrudan erişiyoruz çünkü kod sunucuda çalışıyor. API endpoint'i bile yazmadık. SaaS geliştirirken bu çok şey değiştiriyor; CRUD listelerin %70'i artık tek dosya.
Bir bileşene interaktivite gerektiğinde dosyanın en üstüne 'use client' yazıyorsunuz. O an itibariyle bileşen ve onun import ettiği her şey tarayıcıya gönderilir.
'use client'; import { useState } from 'react';
export default function FilterBar({ initialQuery }: { initialQuery: string }) { const [query, setQuery] = useState(initialQuery); return ( setQuery(e.target.value)} placeholder="Görevlerde ara..." /> ); }
Yeni başlayanların yaptığı en sık hata: tüm uygulamayı 'use client' ile sarıp Pages Router gibi kullanmak. Bunu yaparsanız Next.js 14'ün getirdiği hiçbir avantajı kullanmamış olursunuz, sadece daha karmaşık bir Pages Router yazmış olursunuz. Kuralı şöyle koyun: bileşen useState, useEffect veya browser API'si gerektirmiyorsa Server Component kalsın. Çoğu liste, çoğu detay sayfası, çoğu form parent'ı sunucuda kalabilir.
Veri çekmenin pratik kalıpları
Server Component'lerde await doğrudan çalışıyor. Ama gerçek bir SaaS'ta veriyi nereden çektiğiniz önemli. Üç senaryo:
Sayfa yüklenirken statik veri: Veritabanına doğrudan sorgu, Prisma veya Drizzle ile. Auth kontrolünü auth() çağrısıyla aynı dosyada yaparsınız. getServerSideProps'a göre çok daha temiz.
İstemci tarafından tetiklenen veri: Kullanıcı bir filtre değiştirdiğinde, arama yaptığında. Burada iki seçenek var: bir Route Handler (app/api/...) yazıp useSWR ile çekmek, ya da Server Action kullanıp useTransition ile state güncellemek. Basit listeler için Server Action genelde daha az kod.
Gerçek zamanlı: Bildirimler, canlı yorumlar. Burada Next.js'in tek başına çözümü yok. Pusher, Ably veya kendi WebSocket sunucunuz gerekiyor. Bunu Vercel'in Edge runtime'ında çalıştırmak hâlâ sancılı, ayrı bir process'le çözmek daha sağlıklı.
Layout'lar ve nested routing
App Router'ın sevdiğim tarafı bu. layout.tsx dosyaları iç içe geçiyor ve sadece ilk yüklemede render ediliyor. SaaS dashboard'ları için ideal: dış layout authentication'ı kontrol eder, sidebar'ı render eder. İç layout proje seçicisini render eder. Sayfa değiştiğinde sidebar yeniden render edilmez, sadece içerik değişir.
// app/(app)/layout.tsx
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';
import Sidebar from '@/components/sidebar';
export default async function AppLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
if (!session) redirect('/login');
return (
{children}
);
}Parantez içindeki (app) bir route group. URL'i etkilemiyor ama bu layout'un sadece belirli sayfalarda geçerli olmasını sağlıyor. Marketing sayfaları (/pricing, /features/teams) farklı bir layout kullanabiliyor. Bu, SaaS'larda landing + uygulama ayrımını net tutmayı kolaylaştırıyor; biz proje yönetimi ve pricing sayfalarını bu kalıpla ayırıyoruz.
Server Actions: form ve mutasyonlar
Eskiden bir form göndermek için API endpoint yazıyordunuz, fetch ile çağırıyordunuz, loading state tutuyordunuz. Server Actions bunu büyük ölçüde ortadan kaldırıyor.
// app/projects/actions.ts 'use server'; import { revalidatePath } from 'next/cache'; import { db } from '@/lib/db'; import { auth } from '@/lib/auth';export async function createProject(formData: FormData) { const session = await auth(); if (!session) throw new Error('Yetkisiz');
const name = formData.get('name') as string; if (!name?.trim()) return { error: 'İsim boş olamaz' };
await db.project.create({ data: { name, ownerId: session.user.id }, });
revalidatePath('/projects'); return { ok: true }; }
Form bileşeninizden action={createProject} diye çağırıyorsunuz. JavaScript kapalı olsa bile çalışır (progressive enhancement). revalidatePath Next.js'e bu rotadaki cache'i tazele diyor; sayfa yeniden yüklendiğinde yeni proje görünüyor.
Dikkat etmeniz gereken bir nokta: Server Action içinde validation ve auth'u tekrar yapmak zorundasınız. İstemcide yapılan kontroller her zaman atlatılabilir. zod ile schema validation alışkanlığı edinin, ileride güvenlik açıkları kapatırken çok zaman kazandırıyor.
Auth ve korumalı rotalar
Çoğu yeni başlayan auth'u kendi yazmaya kalkıyor. Yapmayın. NextAuth (artık Auth.js) veya Clerk kullanın. Auth.js bedava ve esnek, Clerk daha hızlı kurulum sunuyor ama bedeli var. Layout'larda session kontrolü yapmak ve middleware.ts ile bütün /dashboard/* altını korumak yeterli. Her sayfada tekrar tekrar auth kontrolü yazmak yerine route grouping kullanın, yukarıda gösterdiğim (app) kalıbı tam bunun için.
Yeni başlayanların ilk hafta yaptığı klasik hatalar
Veritabanı çağrısını Client Component'e taşımak. 'use client' yazılı bir dosyaya Prisma import ederseniz build patlar veya bağımlılıklar tarayıcıya gider. Veriyi parent Server Component'te çekin, prop olarak indirin.
fetch'i Server Component'te yanlış cache'lemek. Next.js 14, fetch'i varsayılan olarak cache'liyor. Eğer her istekte taze veri istiyorsanız { cache: 'no-store' } veya revalidate: 0 belirtmeniz gerekiyor. Aksi halde dashboard'ınız kullanıcılara eski veri gösterir, debug ederken saçınızı yolarsınız.
Image bileşenini atlamak. next/image sadece performans değil; layout shift'i de engelliyor. Statik kullandığınız her yerde Lighthouse skorunuz düşüyor ve mobil kullanıcılar acı çekiyor.
Çok fazla 'use client'. Sayfayı render eden tüm ağacı client'a taşırsanız Server Components'tan tek bir avantaj göremezsiniz. Görev listesi gibi büyük listelerde özellikle fark ediyor: server'da render edip içine küçük client adacıkları yerleştirin.
Metadata ve SEO
SaaS için SEO genelde marketing sayfalarında önemli. App Router'da her page.tsx veya layout.tsx bir metadata objesi export edebilir.
export const metadata = {
title: 'Proje Yönetimi · Poitim',
description: 'Görevleri, ekipleri, takvimi tek yerden yönetin.',
};Dinamik sayfalar için generateMetadata async fonksiyonu kullanılıyor. Blog yazıları, kullanıcı profilleri, public proje sayfaları için bu hayat kurtarıyor. Open Graph görselleri için opengraph-image.tsx dosyalarıyla otomatik üretim yapabilirsiniz, çoğu SaaS bunu hâlâ kaçırıyor ve LinkedIn paylaşımları boş görsellerle çıkıyor.
Deploy ve sonrası
Vercel'de deploy etmek üç tıklık iş. Ama Vercel tek seçenek değil. Ekiple birlikte çalışırken Coolify, Railway veya kendi VPS'iniz de gayet iyi. Vercel'in Edge functions'ları cazip ama database connection pooling, cron job, uzun süren işlemler için kendi sunucunuz çoğu zaman daha pratik. Buna ileride yazıyı genişletirim, şimdilik deploy aşamasında NEXT_PUBLIC_* değişkenlerinin build time'da derlendiğini unutmayın. Çalışma zamanında değiştiremezsiniz, her ortam için yeniden build gerekir.
Eğer tüm bu kalıpları gerçek bir üründe görmek istiyorsanız, ekibinizle birlikte küçük bir SaaS prototipi kurarken Poitim'in demo hesabını deneyebilirsiniz; biz de tam olarak bu yapıyla geliştiriyoruz, görev/proje/takvim akışlarını Next.js 14 üzerinde sürdürüyoruz.
Nereden başlamalı
Bir hafta sürecek küçük bir proje seçin: kişisel bookmark yöneticisi, basit bir CRM, mini bir blog motoru. Auth ekleyin, bir veritabanı bağlayın, bir liste sayfası, bir detay sayfası, bir form. Bu kadarını bitirdiğinizde Next.js 14'ü öğrenmiş sayılırsınız. Geri kalanı detay; ihtiyaç çıktıkça dokümana bakarsınız. Önemli olan ilk hafta dosya açıp gerçek bir şey deploy etmek, video izleyip not almak değil.