Ders 04: Server Functions - Giriş
🎯 Bu Derste Neleri Öğreneceksiniz?
Section titled “🎯 Bu Derste Neleri Öğreneceksiniz?”- Server Functions nedir ve neden kullanılır?
- İlk server function’ınızı nasıl yazarsınız?
- Client’ten server fonksiyon çağırma
- Input validation ile güvenli API’lar oluşturma
- POST requests ile form handling
📚 Server Functions Nedir?
Section titled “📚 Server Functions Nedir?”Server Functions, sadece sunucuda çalışan kod parçalarıdır. Client kodunuzdan sanki normal bir fonksiyonmuş gibi çağırabilirsiniz, ancak aslında sunucuda çalışırlar.
Neden Server Functions Kullanmalıyız?
Section titled “Neden Server Functions Kullanmalıyız?”| Senaryo | Server Function | Normal Fonksiyon |
|---|---|---|
| Database’e erişim | ✅ Güvenli | ❌ Güvensiz |
| API keys kullanımı | ✅ Gizli kalır | ❌ Client’te görünür |
| Dosya işlemleri | ✅ Sunucuda yapılır | ❌ Tarayıcıda çalışmaz |
| Gizli veriler | ✅ Korumalı | ❌ Herkes görebilir |
Temel Kavramlar
Section titled “Temel Kavramlar”// ❌ Yanlış - Client'te çalışmazasync function getUser(id: string) { return db.users.find({ where: { id } })}
// ✅ Doğru - Server functionconst getUser = createServerFn() .inputValidator((id: string) => id) .handler(async ({ data: id }) => { return db.users.find({ where: { id } }) })🚀 İlk Server Function
Section titled “🚀 İlk Server Function”GET Request ile Veri Çekme
Section titled “GET Request ile Veri Çekme”import { createFileRoute } from '@tanstack/react-router'import { createServerFn } from '@tanstack/react-start'
// Server function tanımlaconst getZaman = createServerFn({ method: 'GET' }).handler(async () => { // Bu kod sadece sunucuda çalışır! return { saat: new Date().toLocaleTimeString('tr-TR'), tarih: new Date().toLocaleDateString('tr-TR'), }})
export const Route = createFileRoute('/')({ component: HomePage, loader: async () => { // Server function'ı çağır return await getZaman() },})
function HomePage() { // Loader'dan gelen veriye eriş const data = Route.useLoaderData()
return ( <div style={{ padding: '2rem', textAlign: 'center' }}> <h1>Merhaba!</h1> <p style={{ fontSize: '1.2rem' }}> Şu an saat: <strong style={{ color: '#3b82f6' }}>{data.saat}</strong> </p> <p style={{ fontSize: '1rem', color: '#6b7280' }}> Tarih: {data.tarih} </p> </div> )}Component İçinde Server Function Çağırma
Section titled “Component İçinde Server Function Çağırma”import { createFileRoute } from '@tanstack/react-router'import { createServerFn, useServerFn } from '@tanstack/react-start'
const selamla = createServerFn({ method: 'POST' }) .inputValidator((isim: string) => isim) .handler(async ({ data: isim }) => { return `Merhaba, ${isim}! Server'dan selamlar! 👋` })
export const Route = createFileRoute('/selamla')({ component: SelamlamaPage,})
function SelamlamaPage() { // Server function hook ile const selamlaFn = useServerFn(selamla) const [isim, setIsim] = React.useState('') const [mesaj, setMesaj] = React.useState('')
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const result = await selamlaFn({ data: isim }) setMesaj(result) }
return ( <div style={{ padding: '2rem', maxWidth: '500px', margin: '0 auto' }}> <h1>Selamlama Sayfası</h1>
<form onSubmit={handleSubmit} style={{ marginBottom: '2rem' }}> <input type="text" value={isim} onChange={(e) => setIsim(e.target.value)} placeholder="İsminizi girin" style={{ width: '100%', padding: '0.75rem', marginBottom: '1rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> <button type="submit" disabled={!isim || !!mesaj} style={{ width: '100%', padding: '0.75rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: isim ? 'pointer' : 'not-allowed', opacity: !isim || !!mesaj ? 0.5 : 1, }} > Selamla! </button> </form>
{mesaj && ( <div style={{ padding: '1rem', backgroundColor: '#d1fae5', borderRadius: '4px', border: '1px solid #10b981', }} > <p style={{ margin: 0 }}>{mesaj}</p> </div> )} </div> )}✅ Input Validation ile Güvenli API’lar
Section titled “✅ Input Validation ile Güvenli API’lar”Client’ten gelen veriyi her zaman doğrulayın! Bunun için Zod kullanacağız.
Zod ile Validation
Section titled “Zod ile Validation”# Zod'u yükleyinpnpm add zodimport { createFileRoute } from '@tanstack/react-router'import { createServerFn, useServerFn } from '@tanstack/react-start'import { zodValidator } from '@tanstack/zod-adapter'import { z } from 'zod'
// Validation şemasıconst todoSchema = z.object({ baslik: z.string().min(3, 'En az 3 karakter olmalı'), aciklama: z.string().max(200, 'En fazla 200 karakter'), oncelik: z.enum(['dusuk', 'orta', 'yuksek']).default('orta'),})
// Server function with validationconst todoEkle = createServerFn({ method: 'POST' }) .inputValidator(zodValidator(todoSchema)) .handler(async ({ data }) => { // data artik type-safe ve validated! const yeniTodo = { id: Date.now(), ...data, durum: 'aktif', }
// Database'e kaydet (mock) console.log('Yeni todo:', yeniTodo)
return yeniTodo })
export const Route = createFileRoute('/todo-ekle')({ component: TodoEklePage,})
function TodoEklePage() { const todoEkleFn = useServerFn(todoEkle) const [form, setForm] = React.useState({ baslik: '', aciklama: '', oncelik: 'orta', }) const [sonuc, setSonuc] = React.useState(null)
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault()
try { const result = await todoEkleFn({ data: form }) setSonuc(result) setForm({ baslik: '', aciklama: '', oncelik: 'orta' }) } catch (error: any) { setSonuc({ hata: error.message }) } }
return ( <div style={{ padding: '2rem', maxWidth: '600px', margin: '0 auto' }}> <h1>Todo Ekle</h1>
{sonuc?.hata ? ( <div style={{ padding: '1rem', backgroundColor: '#fee2e2', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #ef4444', }}> Hata: {sonuc.hata} </div> ) : sonuc?.id ? ( <div style={{ padding: '1rem', backgroundColor: '#d1fae5', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #10b981', }}> Todo başarıyla eklendi! ID: {sonuc.id} </div> ) : null}
<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 })} placeholder="Todo başlığı..." style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Açıklama: </label> <textarea value={form.aciklama} onChange={(e) => setForm({ ...form, aciklama: e.target.value })} placeholder="Açıklama..." rows={3} style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Öncelik: </label> <select value={form.oncelik} onChange={(e) => setForm({ ...form, oncelik: e.target.value as any })} style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} > <option value="dusuk">Düşük</option> <option value="orta">Orta</option> <option value="yuksek">Yüksek</option> </select> </div>
<button type="submit" disabled={!form.baslik} style={{ padding: '0.75rem 1.5rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: form.baslik ? 'pointer' : 'not-allowed', opacity: !form.baslik ? 0.5 : 1, }} > Ekle </button> </form> </div> )}Custom Error Messages
Section titled “Custom Error Messages”import { z } from 'zod'
const kullaciSchema = z .object({ email: z.string().email('Geçerli bir email adresi girin'), sifre: z .string() .min(8, 'Şifre en az 8 karakter olmalı') .regex(/[A-Z]/, 'En az bir büyük harf içermeli') .regex(/[0-9]/, 'En az bir rakam içermeli'), yas: z .number() .min(18, '18 yaşından büyük olmalısınız') .max(120, 'Geçerli bir yaş girin'), }) .refine((data) => ({ ...data, sifreTekrar: data.sifre, // Şifre tekrarı }))
const kayitOl = createServerFn({ method: 'POST' }) .inputValidator(zodValidator(kullaciSchema)) .handler(async ({ data }) => { // Validasyon başarılı, veri kullanıma hazır return { success: true, email: data.email } })📡 POST Requests ile Form Handling
Section titled “📡 POST Requests ile Form Handling”Form Data ile Server Function
Section titled “Form Data ile Server Function”import { createFileRoute } from '@tanstack/react-router'import { createServerFn, useServerFn } from '@tanstack/react-start'
const formGonder = createServerFn({ method: 'POST' }) .inputValidator((formData: FormData) => { // FormData'dan veri çıkar return { ad: formData.get('ad'), email: formData.get('email'), mesaj: formData.get('mesaj'), } }) .handler(async ({ data }) => { // E-posta gönderme (mock) console.log('Form verisi:', data)
// Gerçekte burada email gönderme işlemi yapılır // await emailService.send({ ...data })
return { success: true } })
export const Route = createFileRoute('/iletisim')({ component: IletisimPage,})
function IletisimPage() { const formGonderFn = useServerFn(formGonder) const [gonderildi, setGonderildi] = React.useState(false) const [hata, setHata] = React.useState('')
const handleSubmit = async (formData: FormData) => { setHata('') try { await formGonderFn({ data: formData }) setGonderildi(true) } catch (error: any) { setHata(error.message) } }
return ( <div style={{ padding: '2rem', maxWidth: '600px', margin: '0 auto' }}> <h1>İletişim</h1>
{gonderildi ? ( <div style={{ padding: '2rem', backgroundColor: '#d1fae5', borderRadius: '8px', textAlign: 'center', }} > <h2>Mesajınız için teşekkürler!</h2> <p>En kısa sürede size dönüş yapacağız.</p> <button onClick={() => setGonderildi(false)} style={{ padding: '0.5rem 1rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > Başka Form Gönder </button> </div> ) : ( <form action={formGonderFn.url} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Ad Soyad: </label> <input type="text" name="ad" required placeholder="Adınız Soyadınız" style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d55db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> E-posta: </label> <input type="email" name="email" required placeholder="ornek@email.com" style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Mesajınız: </label> <textarea name="mesaj" rows={5} required placeholder="Mesajınızı buraya yazın..." style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
{hata && ( <div style={{ padding: '1rem', backgroundColor: '#fee2e2', borderRadius: '4px', border: '1px solid #ef4444', }} > Hata: {hata} </div> )}
<button type="submit" style={{ padding: '0.75rem 1.5rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > Gönder </button> </form> )} </div> )}💡 İpucu:
formGonderFn.urlkullanarak HTML formunun native submit özelliğini kullanabilirsiniz. JavaScript kapalıyken bile form çalışır!
🔒 Server-Only Code (Gizli Kodlar)
Section titled “🔒 Server-Only Code (Gizli Kodlar)”Bazı kodlar sadece sunucuda çalışmalıdır (API keys, database bağlantısı vb.).
createServerOnlyFn Kullanımı
Section titled “createServerOnlyFn Kullanımı”import { createServerOnlyFn } from '@tanstack/react-start'
// Bu fonksiyon sadece sunucuda çalışırconst getApiKey = createServerOnlyFn(() => { return process.env.SECRET_API_KEY})
const dbConnection = createServerOnlyFn(() => { return createDatabaseConnection()})
// ❌ Client'te çağrılırsa HATA verir!// ✅ Sadece başka bir server function içinde kullanılabilir
const fetchUserData = createServerFn({ method: 'GET' }) .handler(async () => { // Doğru kullanım const apiKey = getApiKey() // ✅ Güvenli const db = dbConnection() // ✅ Güvenli
return db.users.findMany() })Environment Variables Güvenli Kullanımı
Section titled “Environment Variables Güvenli Kullanımı”import { createServerFn } from '@tanstack/react-start'
// ❌ YANLIŞ - Environment variable client'e sızarconst getEnv = createServerFn().handler(async () => { // Bu kötü! Client'te görülebilir return { apiKey: process.env.API_KEY }})
// ✅ DOĞRU - Sadece server'da kullanconst getData = createServerFn().handler(async () => { // API key sadece server function içinde kullanılır const response = await fetch(process.env.API_URL + '/data', { headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, // Güvenli }, })
return response.json()})🎨 Pratik Örnek: Login Sistemi
Section titled “🎨 Pratik Örnek: Login Sistemi”Şimdi öğrendiklerimizle basit bir login sistemi yapalım!
import { createFileRoute, useNavigate, redirect } from '@tanstack/react-router'import { createServerFn, useServerFn } from '@tanstack/react-start'import { zodValidator, z } from 'zod'
// Mock kullanıcı veritabanıconst KULLANICILAR = [ { id: 1, email: 'admin@ornek.com', sifre: 'admin123', ad: 'Admin' }, { id: 2, email: 'user@ornek.com', sifre: 'user123', ad: 'Kullanıcı' },]
// 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ı bul const kullanici = KULLANICILAR.find( (k) => k.email === data.email && k.sifre === data.sifre )
if (!kullanici) { throw new Error('Geçersiz email veya şifre!') }
// Gerçekte burada session oluşturulur // return { kullanici, token: 'jwt-token' }
return { kullanici: { id: kullanici.id, email: kullanici.email, ad: kullanici.ad, }, } })
export const Route = createFileRoute('/login')({ component: LoginPage,})
function LoginPage() { const navigate = useNavigate() const loginFn = useServerFn(login)
const [hata, setHata] = React.useState('') const [yukleniyor, setYukleniyor] = React.useState(false)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() setHata('') setYukleniyor(true)
try { const formData = new FormData(e.currentTarget) const email = formData.get('email') as string const sifre = formData.get('sifre') as string
const result = await loginFn({ data: { email, sifre }, })
// Login başarılı, ana sayfaya yönlendir alert(`Hoş geldin, ${result.kullanici.ad}!`) navigate({ to: '/' }) } catch (error: any) { setHata(error.message || 'Bir hata oluştu') } finally { setYukleniyor(false) } }
return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', backgroundColor: '#f3f4f6', }} > <div style={{ backgroundColor: 'white', padding: '2rem', borderRadius: '8px', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', width: '100%', maxWidth: '400px', }} > <h1 style={{ textAlign: 'center', marginBottom: '1.5rem' }}>Giriş Yap</h1>
{hata && ( <div style={{ padding: '0.75rem', backgroundColor: '#fee2e2', color: '#b91c1c', borderRadius: '4px', marginBottom: '1rem', }} > {hata} </div> )}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> E-posta: </label> <input type="email" name="email" required placeholder="ornek@email.com" style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<div> <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}> Şifre: </label> <input type="password" name="sifre" required placeholder="••••••••" style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> </div>
<button type="submit" disabled={yukleniyor} style={{ padding: '0.75rem 1.5rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: yukleniyor ? 'not-allowed' : 'pointer', opacity: yukleniyor ? 0.6 : 1, }} > {yukleniyor ? 'Giriş yapılıyor...' : 'Giriş Yap'} </button> </form>
<p style={{ textAlign: 'center', marginTop: '1rem', fontSize: '0.9rem', color: '#6b7280' }}> Test hesapları:<br /> admin@ornek.com / admin123<br /> user@ornek.com / user123 </p> </div> </div> )}✅ Ders 4 Özeti
Section titled “✅ Ders 4 Özeti”Bu derste öğrendiklerimiz:
| Konu | Açıklama |
|---|---|
| createServerFn() | Server function oluşturma |
| method | HTTP method’i (GET, POST, vb.) |
| inputValidator | Input validation (Zod) |
| handler | Server-side kod |
| useServerFn() | Client’te çağırma hook’u |
| zodValidator() | Zod ile type-safe validation |
| FormData | Form handling |
| createServerOnlyFn() | Server-only kod güvenliği |
📝 Alıştırmalar
Section titled “📝 Alıştırmalar”Alıştırma 1: Hava Durumu Uygulaması
Section titled “Alıştırma 1: Hava Durumu Uygulaması”Şehir adına göre hava durumu getiren bir uygulama yapın:
const havaDurumuGetir = createServerFn({ method: 'GET' }) .inputValidator((sehir: string) => sehir) .handler(async ({ data: sehir }) => { // Mock API const durumlar = ['güneşli', 'bulutlu', 'yağmurlu', 'karlı'] return { sehir, durum: durumlar[Math.floor(Math.random() * durumlar.length)], sicaklik: Math.floor(Math.random() * 30) + 5, } })Alıştırma 2: Anket Uygulaması
Section titled “Alıştırma 2: Anket Uygulaması”Kullanıcıların anket dolduğu bir uygulama:
- POST server function ile anek kaydetme
- Input validation ile anket verilerini doğrulama
- Başarı mesajı gösterme
Alıştırma 3: Sayaç Uygulaması
Section titled “Alıştırma 3: Sayaç Uygulaması”Ziyaretçi sayacı yapın:
- Her sayfa görüntülendiğinde server function çağır
- Sayacıyı artır ve sonucu göster
🚀 Sonraki Ders: Server Functions - Orta Seviye
Section titled “🚀 Sonraki Ders: Server Functions - Orta Seviye”Bir sonraki derste şunları öğreneceksiniz:
- 🗄️ Context ve middleware ile gelişmiş server functions
- 🔗 Server functions arası bağımlılık
- 📂 File operations (dosya okuma/yazma)
- 🍪 Session ve cookie yönetimi
- ⚡ Error handling ve retry mekanizmaları
💬 Sorularınız?
Section titled “💬 Sorularınız?”- Server function her zaman sunucuda mı çalışır?
- Input validation olmadan ne olur?
- Form data’nı nasıl alırız?
Bir sonraki derste görüşmek üzere! 👋