Skip to content

Ders 05: Server Functions - Orta Seviye

  • 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, server function’lara ek veri aktarmak için kullanılır. Middleware’den gelen verileri alabilir veya yeni veri ekleyebilirsiniz.

import { createMiddleware, createServerFn } from '@tanstack/react-start'
// Auth middleware - kullanıcı bilgisini context'e ekler
const 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ır
const 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,
}
})
types/context.ts
export interface AuthContext {
kullanici: {
id: string
ad: string
email: string
rol: 'admin' | 'user'
}
}
// Module augmentation ile context tip tanımlama
declare module '@tanstack/react-start' {
interface Register {
server: {
context: AuthContext
}
}
}

Middleware, server function’lardan önce veya sonra çalışan ara katmanlardır. İstekClient ve istemServer olmak üzere iki tarafı da vardır.

import { createMiddleware } from '@tanstack/react-start'
// Client middleware - client'te çalışır
const 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
})
// 1. Logging middleware
const 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ım
const secureApiCall = createServerFn({ method: 'GET' })
.middleware([rateLimitMiddleware]) // Tüm middleware'leri çalıştır
.handler(async () => {
return { data: 'Gizli veri' }
})

TanStack Start ile sunucuda dosya okuyup yazabilirsiniz.

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>
)
}
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>
)
}

import { createServerFn } from '@tanstack/react-start'
import { useSession } from '@tanstack/react-start/server'
type SessionData = {
userId?: string
kullaniciAd?: string
rol?: 'admin' | 'user'
}
// Session utility
export 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 function
const 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'ı al
const getSessionData = createServerFn({ method: 'GET' }).handler(async () => {
const session = await getSession()
return session.data
})
// Logout
const logout = createServerFn({ method: 'POST' }).handler(async () => {
const session = await getSession()
await session.clear()
return { basari: true }
})
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>
)
}

src/start.ts
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],
}))
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())
)
})

Şimdi öğrendiklerimizle bir blog için admin paneli yapalım!

// src/routes/admin/bloglar/$yaziSlug/duzenle.tsx
import { createFileRoute, useNavigate, redirect } from '@tanstack/react-router'
import { createServerFn, useServerFn } from '@tanstack/react-start'
import { zodValidator, z } from 'zod'
// Mock blog verileri
const 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ı getir
const 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üncelle
const 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>
)
}

Bu derste öğrendiklerimiz:

KonuAçıklama
ContextServer functions’a veri aktarma
MiddlewareAra katmanlar, zincirleme
createMiddleware()Middleware oluşturma
File I/ODosya okuma/yazma (fs/promises)
useSession()Session yönetimi
Error handlingTry-catch ve retry pattern’ları
Not FoundnotFound() ile 404

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
// ...
})

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 } })
})

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

  1. Middleware sırası önemli mi?
  2. Session’larda neler saklanmamalı?
  3. Dosya işlemlerinde hata yönetimi nasıl olmalı?

Bir sonraki derste görüşmek üzere! 👋