C
Genel Bakış
Next.js Form Validasyon Yöntemleri: Client ve Server Side Doğrulama Rehberi

Next.js Form Validasyon Yöntemleri: Client ve Server Side Doğrulama Rehberi

14 Haziran 2025
9 dk okuma süresi
index

Next.js Form Validasyon Yöntemleri: Client ve Server Side Doğrulama Rehberi

Not (Modern Form Validasyon)

Bu yazıda Next.js 14+ ile modern form validasyon yöntemlerini keşfedeceksin. Client-side ve server-side validasyon tekniklerini, TypeScript ile tip güvenliği sağlamayı ve performans optimizasyonlarını öğreneceksin.

Form validasyonu web geliştirmede kritik bir konudur. Kullanıcı deneyimini iyileştirmek, güvenlik açıklarını önlemek ve veri bütünlüğünü sağlamak için doğru validasyon stratejileri gereklidir. Bu yazıda Next.js’te modern form validasyon yöntemlerini detaylı örneklerle anlatacağım.

Form Validasyon Temelleri

Tanım (Validasyon Katmanları)

Modern web uygulamalarında 3 katmanlı validasyon yaklaşımı önerilir:


1. Client-side: Hızlı geri bildirim ve UX iyileştirmesi


2. Server-side: Güvenlik ve veri bütünlüğü


3. Database level: Son güvenlik katmanı

React Hook Form + Zod ile Modern Validasyon

Ipucu (Kurulum)
npm install react-hook-form zod @hookform/resolvers
npm install -D @types/node

Temel Schema Tanımlama

Örnek (Zod Schema Örneği)
// lib/schemas/auth.ts
import { z } from 'zod'
 
export const loginSchema = z.object({
  email: z
    .string()
    .min(1, 'Email gerekli')
    .email('Geçerli bir email adresi girin'),
  password: z
    .string()
    .min(8, 'Şifre en az 8 karakter olmalı')
    .regex(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
      'Şifre en az bir küçük harf, büyük harf ve sayı içermeli'
    ),
})
 
export const registerSchema = z.object({
  name: z
    .string()
    .min(2, 'İsim en az 2 karakter olmalı')
    .max(50, 'İsim en fazla 50 karakter olabilir'),
  email: z
    .string()
    .min(1, 'Email gerekli')
    .email('Geçerli bir email adresi girin'),
  password: z
    .string()
    .min(8, 'Şifre en az 8 karakter olmalı'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Şifreler eşleşmiyor',
  path: ['confirmPassword'],
})
 
export type LoginFormData = z.infer<typeof loginSchema>
export type RegisterFormData = z.infer<typeof registerSchema>

Client-Side Form Komponenti

Örnek (React Hook Form ile Form Komponenti)
// components/forms/LoginForm.tsx
'use client'
 
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { loginSchema, type LoginFormData } from '@/lib/schemas/auth'
 
export default function LoginForm() {
  const [isSubmitting, setIsSubmitting] = useState(false)
  
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    setError,
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    mode: 'onBlur', // Validasyon tetikleme modu
  })
 
  const onSubmit = async (data: LoginFormData) => {
    setIsSubmitting(true)
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      })
 
      const result = await response.json()
 
      if (!response.ok) {
        // Server-side hatalarını form'da göster
        if (result.field) {
          setError(result.field, { message: result.message })
        } else {
          setError('root', { message: result.message })
        }
        return
      }
 
      // Başarılı login sonrası yönlendirme
      window.location.href = '/dashboard'
    } catch (error) {
      setError('root', { message: 'Bir hata oluştu, tekrar deneyin' })
    } finally {
      setIsSubmitting(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          {...register('email')}
          type="email"
          className={`mt-1 block w-full px-3 py-2 border rounded-md ${
            errors.email ? 'border-red-500' : 'border-gray-300'
          }`}
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
        )}
      </div>
 
      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Şifre
        </label>
        <input
          {...register('password')}
          type="password"
          className={`mt-1 block w-full px-3 py-2 border rounded-md ${
            errors.password ? 'border-red-500' : 'border-gray-300'
          }`}
        />
        {errors.password && (
          <p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
        )}
      </div>
 
      {errors.root && (
        <div className="p-3 bg-red-50 border border-red-200 rounded-md">
          <p className="text-sm text-red-700">{errors.root.message}</p>
        </div>
      )}
 
      <button
        type="submit"
        disabled={isSubmitting || !isValid}
        className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Giriş yapılıyor...' : 'Giriş Yap'}
      </button>
    </form>
  )
}

Server Actions ile Server-Side Validasyon

Önemli (Next.js 14 Server Actions)

Server Actions, form verilerini doğrudan server-side’da işlemek için güçlü bir yöntemdir. Bu yaklaşım hem performans hem güvenlik açısından avantaj sağlar.

Server Action Tanımlama

Örnek (Server Action Örneği)
// app/actions/auth.ts
'use server'
 
import { z } from 'zod'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { loginSchema } from '@/lib/schemas/auth'
 
type ActionResult = {
  success: boolean
  message: string
  errors?: Record<string, string[]>
}
 
export async function loginAction(formData: FormData): Promise<ActionResult> {
  // FormData'yı object'e çevir
  const rawData = {
    email: formData.get('email'),
    password: formData.get('password'),
  }
 
  // Server-side validasyon
  const result = loginSchema.safeParse(rawData)
  
  if (!result.success) {
    return {
      success: false,
      message: 'Validasyon hatası',
      errors: result.error.flatten().fieldErrors,
    }
  }
 
  const { email, password } = result.data
 
  try {
    // Kullanıcı doğrulama işlemi
    const user = await authenticateUser(email, password)
    
    if (!user) {
      return {
        success: false,
        message: 'Email veya şifre hatalı',
      }
    }
 
    // Session oluştur
    await createSession(user.id)
    
    // Cache'i yenile
    revalidatePath('/dashboard')
    
  } catch (error) {
    return {
      success: false,
      message: 'Giriş sırasında bir hata oluştu',
    }
  }
 
  // Başarılı giriş sonrası yönlendirme
  redirect('/dashboard')
}
 
async function authenticateUser(email: string, password: string) {
  // Veritabanı sorgusu ve şifre kontrolü
  // Bu kısım gerçek uygulamada hash karşılaştırması yapacak
  const user = await db.user.findUnique({ where: { email } })
  
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return null
  }
  
  return user
}

Server Action ile Form Kullanımı

Örnek (Progressive Enhancement Form)
// components/forms/ServerLoginForm.tsx
import { loginAction } from '@/app/actions/auth'
 
export default function ServerLoginForm() {
  return (
    <form action={loginAction} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          name="email"
          type="email"
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>
 
      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Şifre
        </label>
        <input
          name="password"
          type="password"
          required
          minLength={8}
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>
 
      <button
        type="submit"
        className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700"
      >
        Giriş Yap
      </button>
    </form>
  )
}

Kompleks Form Validasyonu: Ürün Ekleme Formu

Örnek (Gelişmiş Validasyon Örneği)
// lib/schemas/product.ts
import { z } from 'zod'
 
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
 
export const productSchema = z.object({
  name: z
    .string()
    .min(3, 'Ürün adı en az 3 karakter olmalı')
    .max(100, 'Ürün adı en fazla 100 karakter olabilir'),
  description: z
    .string()
    .min(10, 'Açıklama en az 10 karakter olmalı')
    .max(1000, 'Açıklama en fazla 1000 karakter olabilir'),
  price: z
    .number()
    .min(0.01, 'Fiyat 0\'dan büyük olmalı')
    .max(999999.99, 'Fiyat geçerli aralıkta olmalı'),
  category: z
    .string()
    .min(1, 'Kategori seçilmeli'),
  tags: z
    .array(z.string())
    .min(1, 'En az bir etiket eklenmeli')
    .max(10, 'En fazla 10 etiket eklenebilir'),
  images: z
    .array(
      z.object({
        file: z
          .any()
          .refine((file) => file?.size <= MAX_FILE_SIZE, 'Dosya boyutu 5MB\'dan küçük olmalı')
          .refine(
            (file) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
            'Sadece JPEG, PNG ve WebP formatları kabul edilir'
          ),
        alt: z.string().min(1, 'Alt text gerekli'),
      })
    )
    .min(1, 'En az bir resim yüklenmeli')
    .max(5, 'En fazla 5 resim yüklenebilir'),
  variants: z
    .array(
      z.object({
        size: z.string().min(1, 'Beden gerekli'),
        color: z.string().min(1, 'Renk gerekli'),
        stock: z.number().min(0, 'Stok 0 veya pozitif olmalı'),
        sku: z.string().min(1, 'SKU gerekli'),
      })
    )
    .min(1, 'En az bir varyant eklenmeli'),
  isActive: z.boolean().default(true),
  publishDate: z
    .date()
    .min(new Date(), 'Yayın tarihi gelecekte olmalı')
    .optional(),
})
 
export type ProductFormData = z.infer<typeof productSchema>

Real-time Validasyon ve Debouncing

Örnek (Performanslı Real-time Validasyon)
// hooks/useDebounce.ts
import { useEffect, useState } from 'react'
 
export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value)
 
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)
 
    return () => {
      clearTimeout(handler)
    }
  }, [value, delay])
 
  return debouncedValue
}
 
// components/forms/EmailCheckInput.tsx
'use client'
 
import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
 
export default function EmailCheckInput({ onEmailChange }: { onEmailChange: (email: string, isValid: boolean) => void }) {
  const [email, setEmail] = useState('')
  const [isChecking, setIsChecking] = useState(false)
  const [isAvailable, setIsAvailable] = useState<boolean | null>(null)
  const [error, setError] = useState('')
  
  const debouncedEmail = useDebounce(email, 500)
 
  useEffect(() => {
    if (!debouncedEmail) {
      setIsAvailable(null)
      setError('')
      return
    }
 
    // Email format kontrolü
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(debouncedEmail)) {
      setError('Geçerli bir email adresi girin')
      setIsAvailable(false)
      onEmailChange(debouncedEmail, false)
      return
    }
 
    checkEmailAvailability(debouncedEmail)
  }, [debouncedEmail])
 
  const checkEmailAvailability = async (emailToCheck: string) => {
    setIsChecking(true)
    setError('')
    
    try {
      const response = await fetch(`/api/check-email?email=${encodeURIComponent(emailToCheck)}`)
      const result = await response.json()
      
      if (result.available) {
        setIsAvailable(true)
        setError('')
        onEmailChange(emailToCheck, true)
      } else {
        setIsAvailable(false)
        setError('Bu email adresi zaten kullanılıyor')
        onEmailChange(emailToCheck, false)
      }
    } catch (error) {
      setError('Email kontrolü yapılamadı')
      onEmailChange(emailToCheck, false)
    } finally {
      setIsChecking(false)
    }
  }
 
  return (
    <div>
      <label htmlFor="email" className="block text-sm font-medium">
        Email
      </label>
      <div className="relative">
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className={`mt-1 block w-full px-3 py-2 pr-10 border rounded-md ${
            error ? 'border-red-500' : isAvailable === true ? 'border-green-500' : 'border-gray-300'
          }`}
        />
        <div className="absolute inset-y-0 right-0 flex items-center pr-3">
          {isChecking && (
            <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
          )}
          {isAvailable === true && (
            <svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
              <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
            </svg>
          )}
          {isAvailable === false && error && (
            <svg className="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
              <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
            </svg>
          )}
        </div>
      </div>
      {error && (
        <p className="mt-1 text-sm text-red-600">{error}</p>
      )}
      {isAvailable === true && (
        <p className="mt-1 text-sm text-green-600">Email adresi kullanılabilir</p>
      )}
    </div>
  )
}

API Routes ile Validasyon

Örnek (API Route Validasyon)
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { registerSchema } from '@/lib/schemas/auth'
 
export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    
    // Server-side validasyon
    const validationResult = registerSchema.safeParse(body)
    
    if (!validationResult.success) {
      return NextResponse.json(
        {
          success: false,
          message: 'Validasyon hatası',
          errors: validationResult.error.flatten().fieldErrors,
        },
        { status: 400 }
      )
    }
 
    const { name, email, password } = validationResult.data
 
    // Email benzersizliği kontrolü
    const existingUser = await db.user.findUnique({ where: { email } })
    if (existingUser) {
      return NextResponse.json(
        {
          success: false,
          message: 'Bu email adresi zaten kullanılıyor',
          field: 'email',
        },
        { status: 409 }
      )
    }
 
    // Kullanıcı oluşturma
    const hashedPassword = await bcrypt.hash(password, 12)
    const user = await db.user.create({
      data: {
        name,
        email,
        passwordHash: hashedPassword,
      },
    })
 
    return NextResponse.json({
      success: true,
      message: 'Kullanıcı başarıyla oluşturuldu',
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
      },
    })
  } catch (error) {
    console.error('User creation error:', error)
    return NextResponse.json(
      {
        success: false,
        message: 'Sunucu hatası',
      },
      { status: 500 }
    )
  }
}

Performans Optimizasyonları

Ipucu (Form Performans İpuçları)
  • Debouncing: Real-time validasyon için 300-500ms gecikme kullan
  • Field-level validation: Sadece değişen alanları validate et
  • Lazy loading: Büyük form’ları adım adım yükle
  • Optimistic updates: Başarılı validasyon sonrası UI’ı hemen güncelle

Optimized Form Hook

Örnek (Performans Odaklı Form Hook)
// hooks/useOptimizedForm.ts
import { useCallback, useMemo, useRef } from 'react'
import { useForm, FieldPath, FieldValues } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
 
export function useOptimizedForm<T extends FieldValues>(
  schema: z.ZodSchema<T>,
  options?: {
    debounceMs?: number
    validateOnMount?: boolean
  }
) {
  const { debounceMs = 300, validateOnMount = false } = options || {}
  
  const form = useForm<T>({
    resolver: zodResolver(schema),
    mode: 'onTouched',
    reValidateMode: 'onChange',
    shouldFocusError: true,
  })
 
  const debounceRef = useRef<Record<string, NodeJS.Timeout>>({})
 
  const debouncedValidate = useCallback(
    (fieldName: FieldPath<T>, value: any) => {
      if (debounceRef.current[fieldName]) {
        clearTimeout(debounceRef.current[fieldName])
      }
 
      debounceRef.current[fieldName] = setTimeout(() => {
        form.trigger(fieldName)
      }, debounceMs)
    },
    [form, debounceMs]
  )
 
  const optimizedRegister = useCallback(
    (fieldName: FieldPath<T>) => {
      const registration = form.register(fieldName)
      
      return {
        ...registration,
        onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
          registration.onChange(e)
          debouncedValidate(fieldName, e.target.value)
        },
      }
    },
    [form, debouncedValidate]
  )
 
  // Memoized error helper
  const getFieldError = useCallback(
    (fieldName: FieldPath<T>) => {
      return form.formState.errors[fieldName]?.message
    },
    [form.formState.errors]
  )
 
  // Optimized submit with loading state
  const handleSubmit = useMemo(
    () =>
      form.handleSubmit(async (data: T) => {
        // Clear all debounce timers
        Object.values(debounceRef.current).forEach(clearTimeout)
        
        // Perform final validation
        const isValid = await form.trigger()
        if (!isValid) return
        
        return data
      }),
    [form]
  )
 
  return {
    ...form,
    register: optimizedRegister,
    handleSubmit,
    getFieldError,
  }
}

Error Handling ve User Experience

Önemli (Kullanıcı Dostu Hata Yönetimi)
  • Hataları anlaşılır dilde göster
  • Field-level ve form-level hataları ayır
  • Loading state’leri ekle
  • Success feedback’i unutma
  • Network hatalarını graceful handle et

Global Error Boundary

Örnek (Form Error Boundary)
// components/forms/FormErrorBoundary.tsx
'use client'
 
import { Component, ReactNode } from 'react'
 
interface Props {
  children: ReactNode
  fallback?: ReactNode
}
 
interface State {
  hasError: boolean
  error?: Error
}
 
export class FormErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }
 
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, errorInfo: any) {
    console.error('Form error:', error, errorInfo)
    
    // Error reporting service'e gönder
    // analytics.track('form_error', { error: error.message, ...errorInfo })
  }
 
  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="p-4 bg-red-50 border border-red-200 rounded-md">
            <h3 className="text-lg font-medium text-red-800">
              Form Hatası
            </h3>
            <p className="mt-2 text-sm text-red-700">
              Form işlenirken bir hata oluştu. Sayfayı yenilemeyi deneyin.
            </p>
            <button
              onClick={() => window.location.reload()}
              className="mt-3 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
            >
              Sayfayı Yenile
            </button>
          </div>
        )
      )
    }
 
    return this.props.children
  }
}

Video Kaynakları

YouTube Video
Video

Furkan Özay

Sonuç ve Best Practice’ler

Özet (Form Validasyon Best Practice'leri)
✅ Yapılması Gerekenler:
  • Hem client hem server-side validasyon kullan
  • Type-safe validasyon için Zod veya benzer kütüphaneler tercih et
  • Kullanıcı dostu hata mesajları yaz
  • Real-time validasyon için debouncing uygula
  • Loading state’leri ve success feedback’i ekle
  • Progressive enhancement prensibini benimse
❌ Yapılmaması Gerekenler:
  • Sadece client-side validasyon’a güvenme
  • Çok agresif real-time validasyon yapma
  • Genel hata mesajları kullanma
  • Form state’ini global state’te tutma (gerekmedikçe)
  • Validation logic’i tekrarlama
Ipucu (Modern Alternatifler)

Bu yazıda React Hook Form + Zod kombinasyonunu anlattım, ancak diğer popüler seçenekler de mevcut:


  • Formik + Yup: Daha eski ama stabil
  • React Final Form: Minimal bundle size
  • TanStack Form: Framework agnostic
  • Conform: Progressive enhancement odaklı
Not (Daha Fazla Kaynak)

Form validasyon konusunu derinlemesine öğrenmek için: