czayvatar
Czay
Hakkımda
Blog
Projeler
İletişim

© 2025 Czay. All rights reserved.

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

Geri Dön
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)
@types/node ```
</Callout>

### Temel Schema Tanımlama

<Callout title="Zod Schema Örneği" variant="example">

```typescript
// 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;
	}
}
Youtube

Yazılım Hakkında Güncel İçerikler

Yazılım dünyasına dair en güncel içerikler, dersler ve ipuçları. Yeni başlayanlardan profesyonellere kadar herkes için kaynaklar.

Abone Ol
Ücretsiz • Destek Ol
Düzenli İçerik

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:


React Hook Form Docs

Zod Documentation

Next.js Server Actions

Geri Dön