Ders 05: Server Functions - Orta Seviye
🎯 Bu Derste Neleri Öğreneceksiniz?
Section titled “🎯 Bu Derste Neleri Öğreneceksiniz?”- Context ve middleware kullanımı
- Server functions arasında veri paylaşımı
- Dosya işlemleri (okuma/yazma)
- Session ve cookie yönetimi
- Error handling ve retry mekanizmaları
📚 Context Kavramı
Section titled “📚 Context Kavramı”Context, server function’lara ek veri aktarmak için kullanılır. Middleware’den gelen verileri alabilir veya yeni veri ekleyebilirsiniz.
Middleware ile Context Oluşturma
Section titled “Middleware ile Context Oluşturma”import { createMiddleware, createServerFn } from '@tanstack/react-start'
// Auth middleware - kullanıcı bilgisini context'e eklerconst authMiddleware = createMiddleware({ type: 'function' }) .server(async ({ next }) => { // Kullanıcıyı al (mock) const kullanici = await getKullaniciFromSession()
// Context'e kullanıcı bilgisini ekle return next({ context: { kullanici, }, }) })
// Server function middleware kullanırconst profilGetir = createServerFn({ method: 'GET' }) .middleware([authMiddleware]) .handler(async ({ context }) => { // Context'ten kullanıcı bilgisine eriş const { kullanici } = context
return { id: kullanici.id, ad: kullanici.ad, email: kullanici.email, } })Context Tür Tanımlama
Section titled “Context Tür Tanımlama”export interface AuthContext { kullanici: { id: string ad: string email: string rol: 'admin' | 'user' }}
// Module augmentation ile context tip tanımlamadeclare module '@tanstack/react-start' { interface Register { server: { context: AuthContext } }}🔄 Middleware Nedir?
Section titled “🔄 Middleware Nedir?”Middleware, server function’lardan önce veya sonra çalışan ara katmanlardır. İstekClient ve istemServer olmak üzere iki tarafı da vardır.
Middleware Türleri
Section titled “Middleware Türleri”import { createMiddleware } from '@tanstack/react-start'
// Client middleware - client'te çalışırconst logMiddleware = createMiddleware({ type: 'function' }) .client(async ({ next }) => { console.log('[CLIENT] İstek başlıyor...') const result = await next() console.log('[CLIENT] İstek tamamlandı')
return result }) .server(async ({ next }) => { console.log('[SERVER] İstek başlıyor...') const result = await next() console.log('[SERVER] İstek tamamlandı')
return result })Middleware Chain (Zincirleme)
Section titled “Middleware Chain (Zincirleme)”// 1. Logging middlewareconst logMiddleware = createMiddleware({ type: 'function' }) .server(async ({ next }) => { console.log('[LOG] İstek alındı') return next() })
// 2. Auth middleware - log'a bağımlıconst authMiddleware = createMiddleware({ type: 'function' }) .middleware([logMiddleware]) .server(async ({ next }) => { console.log('[AUTH] Kullanıcı kontrol ediliyor...') const user = await checkUser()
if (!user) { throw new Error('Yetkisiz!') }
return next({ context: { user }, }) })
// 3. Rate limit middleware - auth'a bağımlıconst rateLimitMiddleware = createMiddleware({ type: 'function' }) .middleware([authMiddleware]) .server(async ({ next }) => { console.log('[RATE LIMIT] Kontrol ediliyor...') // Rate limit kontrolü return next() })
// Kullanımconst secureApiCall = createServerFn({ method: 'GET' }) .middleware([rateLimitMiddleware]) // Tüm middleware'leri çalıştır .handler(async () => { return { data: 'Gizli veri' } })📂 Dosya İşlemleri
Section titled “📂 Dosya İşlemleri”TanStack Start ile sunucuda dosya okuyup yazabilirsiniz.
Dosya Okuma
Section titled “Dosya Okuma”import { createServerFn } from '@tanstack/react-start'import * as fs from 'node:fs/promises'
const dosyayiOku = createServerFn({ method: 'GET' }) .inputValidator((dosyaYolu: string) => dosyaYolu) .handler(async ({ data: dosyaYolu }) => { try { const icerik = await fs.readFile(dosyaYolu, 'utf-8') return { icerik } } catch (hata) { throw new Error(`Dosya okunamadı: ${hata.message}`) } })
export const Route = createFileRoute('/dosya/$dosyaAdi')({ component: DosyaGoruntulemePage, loader: async ({ params }) => { const dosyaYolu = `./dosyalar/${params.dosyaAdi}.txt` const { icerik } = await dosyayiOku({ data: dosyaYolu }) return { dosyaAdi: params.dosyaAdi, icerik } },})
function DosyaGoruntulemePage() { const { dosyaAdi, icerik } = Route.useLoaderData()
return ( <div style={{ padding: '2rem' }}> <h1>{dosyaAdi}</h1> <pre style={{ backgroundColor: '#f3f4f6', padding: '1rem', borderRadius: '8px', overflow: 'auto', whiteSpace: 'pre-wrap', }} > {icerik} </pre> </div> )}Dosya Yazma
Section titled “Dosya Yazma”import { createServerFn } from '@tanstack/react-start'import * as fs from 'node:fs/promises'
const dosyayaYaz = createServerFn({ method: 'POST' }) .inputValidator( z.object({ dosyaYolu: z.string(), icerik: z.string(), }) ) .handler(async ({ data }) => { try { await fs.writeFile(data.dosyaYolu, data.icerik, 'utf-8') return { basari: true } } catch (hata) { throw new Error(`Dosya yazılamadı: ${hata.message}`) } })
export const Route = createFileRoute('/dosya-yaz')({ component: DosyaYazmaPage,})
function DosyaYazmaPage() { const dosyayaYazFn = useServerFn(dosyayaYaz) const [sonuc, setSonuc] = React.useState('')
const handleSubmit = async (formData: FormData) => { const result = await dosyayaYazFn({ data: { dosyaYolu: formData.get('dosyaYolu'), icerik: formData.get('icerik'), }, }) setSonuc(result.basari ? 'Başarılı!' : 'Hata!') }
return ( <div style={{ padding: '2rem' }}> <h1>Dosya Oluştur</h1>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <input name="dosyaYolu" placeholder="Dosya yolu (örn: ./data/dosya.txt)" required /> <textarea name="icerik" placeholder="Dosya içeriği" rows={5} required /> <button type="submit">Oluştur</button> </form>
{sonuc && <p>{sonuc}</p>} </div> )}🍪 Session ve Cookie Yönetimi
Section titled “🍪 Session ve Cookie Yönetimi”Session Oluşturma
Section titled “Session Oluşturma”import { createServerFn } from '@tanstack/react-start'import { useSession } from '@tanstack/react-start/server'
type SessionData = { userId?: string kullaniciAd?: string rol?: 'admin' | 'user'}
// Session utilityexport function getSession() { return useSession<SessionData>({ name: 'app-session', password: process.env.SESSION_SECRET!, // En az 32 karakter cookie: { secure: process.env.NODE_ENV === 'production', sameSite: 'lax', httpOnly: true, maxAge: 60 * 60 * 24 * 7, // 1 hafta }, })}
// Login server functionconst login = createServerFn({ method: 'POST' }) .inputValidator( z.object({ email: z.string().email(), sifre: z.string().min(6), }) ) .handler(async ({ data }) => { // Kullanıcıyı doğrula const kullanici = await authenticateUser(data.email, data.sifre)
if (!kullanici) { throw new Error('Geçersiz bilgiler') }
// Session oluştur const session = await getSession() await session.update({ userId: kullanici.id, kullaniciAd: kullanici.ad, rol: kullanici.rol, })
return { basari: true } })
// Mevcut session'ı alconst getSessionData = createServerFn({ method: 'GET' }).handler(async () => { const session = await getSession()
return session.data})
// Logoutconst logout = createServerFn({ method: 'POST' }).handler(async () => { const session = await getSession() await session.clear()
return { basari: true }})Session Kullanımı
Section titled “Session Kullanımı”export const Route = createFileRoute('/dashboard')({ component: DashboardPage, loader: async () => { const sessionData = await getSessionData()
if (!sessionData.userId) { throw redirect({ to: '/login', search: { redirect: '/dashboard' }, }) }
return { kullanici: sessionData } },})
function DashboardPage() { const { kullanici } = Route.useLoaderData()
return ( <div> <h1>Dashboard</h1> <p>Hoş geldin, {kullanici.kullaniciAd}!</p> <p>Rol: {kullanici.rol}</p> </div> )}⚡ Error Handling ve Retry
Section titled “⚡ Error Handling ve Retry”Global Error Handler
Section titled “Global Error Handler”import { createStart } from '@tanstack/react-start'
const errorHandlerMiddleware = createMiddleware() .server(async ({ next }) => { try { return await next() } catch (error) { console.error('[ERROR]', error)
// Hata loglama servisine gönder // await logErrorToService(error)
throw error // Hatayı yine fırlat } })
export const startInstance = createStart(() => ({ requestMiddleware: [errorHandlerMiddleware],}))Retry Pattern
Section titled “Retry Pattern”import { createServerFn } from '@tanstack/react-start'
const retryOperation = async <T>( operation: () => Promise<T>, maxDeneme = 3,): Promise<T> => { for (let i = 0; i < maxDeneme; i++) { try { return await operation() } catch (error) { console.log(`Deneme ${i + 1}/${maxDeneme} başarısız`)
if (i === maxDeneme - 1) { throw error }
// Biraz bekle (100ms, 200ms, 400ms...) await new Promise((resolve) => setTimeout(resolve, 100 * (i + 2)) ) } }
throw new Error('Retry işlemi başarısız')}
// Kullanımıconst hassasVerisiGetir = createServerFn({ method: 'GET' }) .handler(async () => { return retryOperation(() => fetch('https://hassas-api.com/veri').then((r) => r.json()) ) })🎨 Pratik Örnek: Blog Yönetim Paneli
Section titled “🎨 Pratik Örnek: Blog Yönetim Paneli”Şimdi öğrendiklerimizle bir blog için admin paneli yapalım!
// src/routes/admin/bloglar/$yaziSlug/duzenle.tsximport { createFileRoute, useNavigate, redirect } from '@tanstack/react-router'import { createServerFn, useServerFn } from '@tanstack/react-start'import { zodValidator, z } from 'zod'
// Mock blog verilericonst BLOG_YAZILARI = [ { id: 1, slug: 'ilk-yazi', baslik: 'İlk Yazım', icerik: 'İlk yazı içeriği...' }, { id: 2, slug: 'ikinci-yazi', baslik: 'İkinci Yazı', icerik: 'İkinci yazı içeriği...' },]
// Yazı getirconst yaziGetir = createServerFn({ method: 'GET' }) .inputValidator((slug: string) => slug) .handler(async ({ data: slug }) => { const yazi = BLOG_YAZILARI.find((y) => y.slug === slug)
if (!yazi) { throw notFound() }
return yazi })
// Yazı güncelleconst yaziGuncelle = createServerFn({ method: 'POST' }) .inputValidator( z.object({ slug: z.string(), baslik: z.string().min(3), icerik: z.string().min(10), }) ) .handler(async ({ data }) => { const index = BLOG_YAZILARI.findIndex((y) => y.slug === data.slug)
if (index === -1) { throw new Error('Yazı bulunamadı') }
// Güncelle (mock) BLOG_YAZILARI[index] = { ...BLOG_YAZILARI[index], ...data, }
return BLOG_YAZILARI[index] })
export const Route = createFileRoute('/admin/bloglar/$yaziSlug/duzenle')({ loader: async ({ params }) => { return await yaziGetir({ data: params.yaziSlug }) }, component: AdminYaziDuzenlePage,})
function AdminYaziDuzenlePage() { const navigate = useNavigate() const { slug } = Route.useParams() const yazi = Route.useLoaderData() const yaziGuncelleFn = useServerFn(yaziGuncelle)
const [form, setForm] = React.useState({ baslik: yazi.baslik, icerik: yazi.icerik, }) const [gonderildi, setGonderildi] = React.useState(false)
const handleSubmit = async () => { try { await yaziGuncelleFn({ data: { slug, ...form }, }) setGonderildi(true) setTimeout(() => navigate({ to: '/admin/bloglar' }), 1000) } catch (error: any) { alert('Hata: ' + error.message) } }
return ( <div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}> <h1>Yazı Düzenle: {yazi.baslik}</h1>
{gonderildi ? ( <div style={{ padding: '1rem', backgroundColor: '#d1fae5', borderRadius: '4px' }}> <p>Kaydedildi! Yönlendiriliyorsunuz...</p> </div> ) : ( <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Başlık: </label> <input type="text" value={form.baslik} onChange={(e) => setForm({ ...form, baslik: e.target.value })} style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> İçerik: </label> <textarea value={form.icerik} onChange={(e) => setForm({ ...form, icerik: e.target.value })} rows={10} style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', fontFamily: 'monospace', }} /> </div>
<button type="submit" style={{ padding: '0.75rem 1.5rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > Güncelle </button>
<button type="button" onClick={() => navigate({ to: '/admin/bloglar' })} style={{ padding: '0.75rem 1.5rem', backgroundColor: '#9ca3af', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > İptal </button> </form> )} </div> )}✅ Ders 5 Özeti
Section titled “✅ Ders 5 Özeti”Bu derste öğrendiklerimiz:
| Konu | Açıklama |
|---|---|
| Context | Server functions’a veri aktarma |
| Middleware | Ara katmanlar, zincirleme |
| createMiddleware() | Middleware oluşturma |
| File I/O | Dosya okuma/yazma (fs/promises) |
| useSession() | Session yönetimi |
| Error handling | Try-catch ve retry pattern’ları |
| Not Found | notFound() ile 404 |
📝 Alıştırmalar
Section titled “📝 Alıştırmalar”Alıştırma 1: Sayaç Uygulaması
Section titled “Alıştırma 1: Sayaç Uygulaması”Her sayfa görüntülendiğinde sayaç artsın:
const sayacArtir = createServerFn({ method: 'POST' }) .inputValidator((sayacAdi: string) => sayacAdi) .handler(async ({ data }) => { // Dosyadan oku, artır, kaydet // ... })Alıştırma 2: Global Auth Middleware
Section titled “Alıştırma 2: Global Auth Middleware”Tüm route’lar için auth middleware ekleyin:
const authCheckMiddleware = createMiddleware() .server(async ({ next }) => { const session = await getSession() if (!session.data.userId) { throw redirect({ to: '/login' }) } return next({ context: { user: session.data } }) })Alıştırma 3: Veritabanı Bağlantısı
Section titled “Alıştırma 3: Veritabanı Bağlantısı”Mock veri yerine gerçek veritabanı bağlantısı:
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL,})
const urunGetir = createServerFn({ method: 'GET' }) .handler(async () => { const result = await pool.query('SELECT * FROM urunler') return result.rows })🚀 Sonraki Ders: State Management ve Data Fetching
Section titled “🚀 Sonraki Ders: State Management ve Data Fetching”Bir sonraki derste şunları öğreneceksiniz:
- 📊 TanStack Query ile client-side state
- 🔄 Server ve client state senkronizasyonu
- 🎯 Optimistic updates
- 📦 Infinite query ve pagination
- ⚡ Cache management
💬 Sorularınız?
Section titled “💬 Sorularınız?”- Middleware sırası önemli mi?
- Session’larda neler saklanmamalı?
- Dosya işlemlerinde hata yönetimi nasıl olmalı?
Bir sonraki derste görüşmek üzere! 👋