[{"data":1,"prerenderedAt":378},["ShallowReactive",2],{"blog-how-to/form-validation":3},{"id":4,"title":5,"body":6,"category":359,"date":360,"dateModified":360,"description":361,"draft":362,"extension":363,"faq":364,"featured":362,"headerVariant":365,"image":364,"keywords":364,"meta":366,"navigation":367,"ogDescription":368,"ogTitle":364,"path":369,"readTime":364,"schemaOrg":370,"schemaType":371,"seo":372,"sitemap":373,"stem":374,"tags":375,"twitterCard":376,"__hash__":377},"blog/blog/how-to/form-validation.md","How to Implement Secure Form Validation",{"type":7,"value":8,"toc":338},"minimark",[9,13,17,21,30,35,51,55,58,68,72,95,111,127,143,159,175,213,217,220,226,232,236,240,246,250,256,260,266,270,276,282,289,295,307,313,319],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-implement-secure-form-validation",[18,19,20],"p",{},"Client-side validation is for UX. Server-side validation is for security.",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR (20 minutes)",[18,28,29],{},"Always validate on the server, even if you have client-side validation. Use Zod schemas shared between client and server. Add CSRF tokens to all state-changing forms. Implement honeypots to catch bots. Rate limit submissions to prevent abuse.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,42,45,48],"ul",{},[39,40,41],"li",{},"Node.js 18+ installed",[39,43,44],{},"React or Next.js project",[39,46,47],{},"Basic understanding of forms and HTTP",[39,49,50],{},"npm or yarn package manager",[31,52,54],{"id":53},"why-secure-form-validation-matters","Why Secure Form Validation Matters",[18,56,57],{},"Forms are the primary entry point for user input and a major attack vector. Without proper validation, attackers can inject malicious data, bypass business rules, and overwhelm your systems with spam.",[59,60,61],"danger-box",{},[18,62,63,67],{},[64,65,66],"strong",{},"Why Client-Side Validation Isn't Enough:"," Users can bypass client-side validation by disabling JavaScript, using browser DevTools, or calling your API directly with tools like curl. Never rely on client-side validation for security.",[31,69,71],{"id":70},"step-by-step-guide","Step-by-Step Guide",[73,74,76,81,84],"step",{"number":75},"1",[77,78,80],"h3",{"id":79},"install-required-packages","Install required packages",[18,82,83],{},"Install Zod for schema validation and React Hook Form for client-side forms:",[85,86,91],"pre",{"className":87,"code":89,"language":90},[88],"language-text","npm install zod react-hook-form @hookform/resolvers\n","text",[92,93,89],"code",{"__ignoreMap":94},"",[73,96,98,102,105],{"number":97},"2",[77,99,101],{"id":100},"define-shared-validation-schemas","Define shared validation schemas",[18,103,104],{},"Create schemas that work on both client and server:",[85,106,109],{"className":107,"code":108,"language":90},[88],"// lib/schemas/contact.ts\nimport { z } from 'zod';\n\nexport const ContactFormSchema = z.object({\n  name: z.string()\n    .min(1, 'Name is required')\n    .max(100, 'Name must be less than 100 characters')\n    .regex(/^[a-zA-Z\\s'-]+$/, 'Name contains invalid characters'),\n\n  email: z.string()\n    .email('Please enter a valid email')\n    .max(254, 'Email is too long')\n    .toLowerCase(),\n\n  subject: z.enum(['general', 'support', 'sales', 'feedback'], {\n    errorMap: () => ({ message: 'Please select a valid subject' }),\n  }),\n\n  message: z.string()\n    .min(10, 'Message must be at least 10 characters')\n    .max(5000, 'Message must be less than 5000 characters'),\n\n  // Honeypot field - should always be empty\n  website: z.string().max(0, 'Invalid submission').optional(),\n\n  // Timestamp for timing-based bot detection\n  _timestamp: z.number().optional(),\n});\n\nexport type ContactFormData = z.infer\u003Ctypeof ContactFormSchema>;\n\n// More schemas for common forms\nexport const SignupFormSchema = z.object({\n  email: z.string().email().max(254).toLowerCase(),\n  password: z.string()\n    .min(8, 'Password must be at least 8 characters')\n    .max(100)\n    .regex(/[A-Z]/, 'Password must contain an uppercase letter')\n    .regex(/[a-z]/, 'Password must contain a lowercase letter')\n    .regex(/[0-9]/, 'Password must contain a number'),\n  confirmPassword: z.string(),\n}).refine(data => data.password === data.confirmPassword, {\n  message: 'Passwords do not match',\n  path: ['confirmPassword'],\n});\n\nexport const LoginFormSchema = z.object({\n  email: z.string().email().max(254).toLowerCase(),\n  password: z.string().min(1, 'Password is required').max(100),\n});\n",[92,110,108],{"__ignoreMap":94},[73,112,114,118,121],{"number":113},"3",[77,115,117],{"id":116},"create-a-secure-form-component","Create a secure form component",[18,119,120],{},"Build a reusable form with client-side validation:",[85,122,125],{"className":123,"code":124,"language":90},[88],"// components/ContactForm.tsx\n'use client';\n\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { ContactFormSchema, ContactFormData } from '@/lib/schemas/contact';\nimport { useState, useEffect } from 'react';\n\nexport function ContactForm() {\n  const [submitStatus, setSubmitStatus] = useState\u003C'idle' | 'loading' | 'success' | 'error'>('idle');\n  const [errorMessage, setErrorMessage] = useState('');\n  const [formLoadTime] = useState(Date.now());\n\n  const {\n    register,\n    handleSubmit,\n    formState: { errors },\n    reset,\n  } = useForm\u003CContactFormData>({\n    resolver: zodResolver(ContactFormSchema),\n  });\n\n  const onSubmit = async (data: ContactFormData) => {\n    setSubmitStatus('loading');\n    setErrorMessage('');\n\n    try {\n      const response = await fetch('/api/contact', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          ...data,\n          _timestamp: formLoadTime,\n        }),\n      });\n\n      const result = await response.json();\n\n      if (!response.ok) {\n        throw new Error(result.error || 'Submission failed');\n      }\n\n      setSubmitStatus('success');\n      reset();\n    } catch (error) {\n      setSubmitStatus('error');\n      setErrorMessage(error instanceof Error ? error.message : 'Something went wrong');\n    }\n  };\n\n  return (\n    \u003Cform onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\n      {/* Honeypot field - hidden from real users */}\n      \u003Cdiv style={{ position: 'absolute', left: '-9999px' }} aria-hidden=\"true\">\n        \u003Clabel htmlFor=\"website\">Website (leave empty)\u003C/label>\n        \u003Cinput\n          type=\"text\"\n          id=\"website\"\n          tabIndex={-1}\n          autoComplete=\"off\"\n          {...register('website')}\n        />\n      \u003C/div>\n\n      \u003Cdiv>\n        \u003Clabel htmlFor=\"name\">Name *\u003C/label>\n        \u003Cinput\n          type=\"text\"\n          id=\"name\"\n          {...register('name')}\n          aria-invalid={errors.name ? 'true' : 'false'}\n        />\n        {errors.name && (\n          \u003Cp className=\"error\" role=\"alert\">{errors.name.message}\u003C/p>\n        )}\n      \u003C/div>\n\n      \u003Cdiv>\n        \u003Clabel htmlFor=\"email\">Email *\u003C/label>\n        \u003Cinput\n          type=\"email\"\n          id=\"email\"\n          {...register('email')}\n          aria-invalid={errors.email ? 'true' : 'false'}\n        />\n        {errors.email && (\n          \u003Cp className=\"error\" role=\"alert\">{errors.email.message}\u003C/p>\n        )}\n      \u003C/div>\n\n      \u003Cdiv>\n        \u003Clabel htmlFor=\"subject\">Subject *\u003C/label>\n        \u003Cselect id=\"subject\" {...register('subject')}>\n          \u003Coption value=\"\">Select a subject\u003C/option>\n          \u003Coption value=\"general\">General Inquiry\u003C/option>\n          \u003Coption value=\"support\">Technical Support\u003C/option>\n          \u003Coption value=\"sales\">Sales\u003C/option>\n          \u003Coption value=\"feedback\">Feedback\u003C/option>\n        \u003C/select>\n        {errors.subject && (\n          \u003Cp className=\"error\" role=\"alert\">{errors.subject.message}\u003C/p>\n        )}\n      \u003C/div>\n\n      \u003Cdiv>\n        \u003Clabel htmlFor=\"message\">Message *\u003C/label>\n        \u003Ctextarea\n          id=\"message\"\n          rows={5}\n          {...register('message')}\n          aria-invalid={errors.message ? 'true' : 'false'}\n        />\n        {errors.message && (\n          \u003Cp className=\"error\" role=\"alert\">{errors.message.message}\u003C/p>\n        )}\n      \u003C/div>\n\n      {submitStatus === 'error' && (\n        \u003Cdiv className=\"error-box\" role=\"alert\">{errorMessage}\u003C/div>\n      )}\n\n      {submitStatus === 'success' && (\n        \u003Cdiv className=\"success-box\" role=\"status\">Message sent successfully!\u003C/div>\n      )}\n\n      \u003Cbutton type=\"submit\" disabled={submitStatus === 'loading'}>\n        {submitStatus === 'loading' ? 'Sending...' : 'Send Message'}\n      \u003C/button>\n    \u003C/form>\n  );\n}\n",[92,126,124],{"__ignoreMap":94},[73,128,130,134,137],{"number":129},"4",[77,131,133],{"id":132},"implement-server-side-validation","Implement server-side validation",[18,135,136],{},"Create the API route with full server-side validation:",[85,138,141],{"className":139,"code":140,"language":90},[88],"// app/api/contact/route.ts\nimport { ContactFormSchema } from '@/lib/schemas/contact';\nimport { rateLimit } from '@/lib/rate-limit';\nimport { sanitizePlainText } from '@/lib/sanitize';\nimport { headers } from 'next/headers';\n\n// Rate limit: 5 requests per minute per IP\nconst limiter = rateLimit({\n  interval: 60 * 1000,\n  uniqueTokenPerInterval: 500,\n});\n\nexport async function POST(request: Request) {\n  // Get client IP for rate limiting\n  const headersList = headers();\n  const ip = headersList.get('x-forwarded-for') || 'anonymous';\n\n  // Check rate limit\n  try {\n    await limiter.check(5, ip);\n  } catch {\n    return Response.json(\n      { error: 'Too many requests. Please try again later.' },\n      { status: 429 }\n    );\n  }\n\n  try {\n    const body = await request.json();\n\n    // Server-side validation\n    const result = ContactFormSchema.safeParse(body);\n\n    if (!result.success) {\n      return Response.json(\n        {\n          error: 'Validation failed',\n          details: result.error.flatten(),\n        },\n        { status: 400 }\n      );\n    }\n\n    const { name, email, subject, message, website, _timestamp } = result.data;\n\n    // Honeypot check - if filled, it's a bot\n    if (website && website.length > 0) {\n      // Log for monitoring but return success to avoid tipping off the bot\n      console.warn('Honeypot triggered:', { ip, email });\n      return Response.json({ success: true });\n    }\n\n    // Timing check - if submitted too quickly, likely a bot\n    if (_timestamp) {\n      const submitTime = Date.now() - _timestamp;\n      if (submitTime \u003C 3000) { // Less than 3 seconds\n        console.warn('Form submitted too quickly:', { ip, submitTime });\n        return Response.json({ success: true });\n      }\n    }\n\n    // Sanitize inputs before storing/sending\n    const sanitizedData = {\n      name: sanitizePlainText(name),\n      email: email.toLowerCase().trim(),\n      subject,\n      message: sanitizePlainText(message),\n    };\n\n    // Store in database or send email\n    await db.contactSubmission.create({\n      data: {\n        ...sanitizedData,\n        ipAddress: ip,\n        submittedAt: new Date(),\n      },\n    });\n\n    // Optional: Send notification email\n    // await sendEmail({ to: 'team@example.com', ... });\n\n    return Response.json({ success: true });\n  } catch (error) {\n    console.error('Contact form error:', error);\n    return Response.json(\n      { error: 'An error occurred. Please try again.' },\n      { status: 500 }\n    );\n  }\n}\n",[92,142,140],{"__ignoreMap":94},[73,144,146,150,153],{"number":145},"5",[77,147,149],{"id":148},"add-csrf-protection-for-traditional-forms","Add CSRF protection for traditional forms",[18,151,152],{},"If using traditional form submissions (not fetch/AJAX), add CSRF tokens:",[85,154,157],{"className":155,"code":156,"language":90},[88],"// lib/csrf.ts\nimport { cookies } from 'next/headers';\nimport { randomBytes, createHmac } from 'crypto';\n\nconst CSRF_SECRET = process.env.CSRF_SECRET!;\n\nexport function generateCsrfToken(): string {\n  const token = randomBytes(32).toString('hex');\n  const timestamp = Date.now().toString();\n  const signature = createHmac('sha256', CSRF_SECRET)\n    .update(`${token}:${timestamp}`)\n    .digest('hex');\n\n  const fullToken = `${token}:${timestamp}:${signature}`;\n\n  // Store in httpOnly cookie\n  cookies().set('csrf-token', fullToken, {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'lax',\n    maxAge: 60 * 60, // 1 hour\n  });\n\n  return token;\n}\n\nexport function verifyCsrfToken(submittedToken: string): boolean {\n  const storedToken = cookies().get('csrf-token')?.value;\n  if (!storedToken) return false;\n\n  const [token, timestamp, signature] = storedToken.split(':');\n\n  // Verify signature\n  const expectedSignature = createHmac('sha256', CSRF_SECRET)\n    .update(`${token}:${timestamp}`)\n    .digest('hex');\n\n  if (signature !== expectedSignature) return false;\n\n  // Check expiry (1 hour)\n  if (Date.now() - parseInt(timestamp) > 60 * 60 * 1000) return false;\n\n  // Compare tokens\n  return token === submittedToken;\n}\n\n// Usage in form page\n// app/contact/page.tsx\nimport { generateCsrfToken } from '@/lib/csrf';\n\nexport default function ContactPage() {\n  const csrfToken = generateCsrfToken();\n\n  return (\n    \u003Cform action=\"/api/contact\" method=\"POST\">\n      \u003Cinput type=\"hidden\" name=\"_csrf\" value={csrfToken} />\n      {/* ... other fields */}\n    \u003C/form>\n  );\n}\n\n// Verify in API route\nconst csrfToken = body._csrf;\nif (!verifyCsrfToken(csrfToken)) {\n  return Response.json({ error: 'Invalid CSRF token' }, { status: 403 });\n}\n",[92,158,156],{"__ignoreMap":94},[73,160,162,166,169],{"number":161},"6",[77,163,165],{"id":164},"implement-rate-limiting","Implement rate limiting",[18,167,168],{},"Create a rate limiter to prevent abuse:",[85,170,173],{"className":171,"code":172,"language":90},[88],"// lib/rate-limit.ts\nimport { LRUCache } from 'lru-cache';\n\ninterface RateLimitOptions {\n  interval: number;\n  uniqueTokenPerInterval: number;\n}\n\nexport function rateLimit(options: RateLimitOptions) {\n  const tokenCache = new LRUCache\u003Cstring, number[]>({\n    max: options.uniqueTokenPerInterval,\n    ttl: options.interval,\n  });\n\n  return {\n    check: (limit: number, token: string): Promise\u003Cvoid> =>\n      new Promise((resolve, reject) => {\n        const tokenCount = tokenCache.get(token) || [0];\n        const currentUsage = tokenCount[0];\n\n        if (currentUsage >= limit) {\n          reject(new Error('Rate limit exceeded'));\n          return;\n        }\n\n        tokenCache.set(token, [currentUsage + 1]);\n        resolve();\n      }),\n  };\n}\n\n// More sophisticated: Use Redis for distributed rate limiting\nimport Redis from 'ioredis';\n\nconst redis = new Redis(process.env.REDIS_URL);\n\nexport async function checkRateLimit(\n  key: string,\n  limit: number,\n  windowMs: number\n): Promise\u003C{ allowed: boolean; remaining: number; resetTime: number }> {\n  const now = Date.now();\n  const windowStart = now - windowMs;\n\n  // Remove old entries and add new one\n  await redis.zremrangebyscore(key, 0, windowStart);\n  const count = await redis.zcard(key);\n\n  if (count >= limit) {\n    const oldestTime = await redis.zrange(key, 0, 0, 'WITHSCORES');\n    const resetTime = parseInt(oldestTime[1]) + windowMs;\n    return { allowed: false, remaining: 0, resetTime };\n  }\n\n  await redis.zadd(key, now, `${now}-${Math.random()}`);\n  await redis.expire(key, Math.ceil(windowMs / 1000));\n\n  return { allowed: true, remaining: limit - count - 1, resetTime: now + windowMs };\n}\n",[92,174,172],{"__ignoreMap":94},[176,177,178,181],"warning-box",{},[18,179,180],{},"Security Checklist",[36,182,183,186,189,192,195,198,201,204,207,210],{},[39,184,185],{},"Always validate on the server (never trust client-side only)",[39,187,188],{},"Use the same Zod schema for client and server validation",[39,190,191],{},"Sanitize all inputs before storing or displaying",[39,193,194],{},"Add honeypot fields to catch bots",[39,196,197],{},"Implement timing-based bot detection (reject instant submissions)",[39,199,200],{},"Add CSRF tokens for cookie-based auth with traditional forms",[39,202,203],{},"Implement rate limiting per IP/user",[39,205,206],{},"Log failed validation attempts for security monitoring",[39,208,209],{},"Return generic error messages (don't reveal validation logic)",[39,211,212],{},"Use secure, httpOnly cookies for session tokens",[31,214,216],{"id":215},"how-to-verify-it-worked","How to Verify It Worked",[18,218,219],{},"Test your form validation security:",[85,221,224],{"className":222,"code":223,"language":90},[88],"// Test form security\n\n// 1. Test client-side bypass\n// Open browser DevTools, disable JavaScript, submit form\n// Server should still validate and reject invalid data\n\n// 2. Test with curl (bypasses all client validation)\ncurl -X POST http://localhost:3000/api/contact \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"\",\"email\":\"notanemail\",\"message\":\"hi\"}'\n# Should return 400 with validation errors\n\n// 3. Test honeypot\ncurl -X POST http://localhost:3000/api/contact \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Bot\",\"email\":\"bot@spam.com\",\"message\":\"Buy now!\",\"website\":\"http://spam.com\"}'\n# Should return success but not actually process\n\n// 4. Test rate limiting\nfor i in {1..10}; do\n  curl -X POST http://localhost:3000/api/contact \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"name\":\"Test\",\"email\":\"test@test.com\",\"subject\":\"general\",\"message\":\"Testing rate limits\"}'\ndone\n# Should start returning 429 after limit\n\n// 5. Test XSS in inputs\ncurl -X POST http://localhost:3000/api/contact \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"\u003Cscript>alert(1)\u003C/script>\",\"email\":\"test@test.com\",\"subject\":\"general\",\"message\":\"Test\"}'\n# Should sanitize or reject the script tag\n",[92,225,223],{"__ignoreMap":94},[227,228,229],"tip-box",{},[18,230,231],{},"Pro Tip:\nUse tools like Burp Suite or OWASP ZAP to test your forms for security vulnerabilities. They can automatically test for injection attacks, CSRF, and other common issues.",[31,233,235],{"id":234},"common-errors-and-troubleshooting","Common Errors and Troubleshooting",[77,237,239],{"id":238},"error-zod-schema-not-validating-on-server","Error: Zod schema not validating on server",[85,241,244],{"className":242,"code":243,"language":90},[88],"// Problem: Using parse() which throws instead of returning errors\nconst data = ContactFormSchema.parse(body); // Throws on invalid\n\n// Solution: Use safeParse() for controlled error handling\nconst result = ContactFormSchema.safeParse(body);\nif (!result.success) {\n  return Response.json({ error: result.error.flatten() }, { status: 400 });\n}\n",[92,245,243],{"__ignoreMap":94},[77,247,249],{"id":248},"error-form-submits-but-csrf-token-is-invalid","Error: Form submits but CSRF token is invalid",[85,251,254],{"className":252,"code":253,"language":90},[88],"// Problem: Token generated on server but not matching\n\n// Solution: Ensure token is generated and passed correctly\n// 1. Generate token in server component\nconst csrfToken = generateCsrfToken();\n\n// 2. Pass to form (not via state that resets)\n\u003Cinput type=\"hidden\" name=\"_csrf\" value={csrfToken} />\n\n// 3. Verify the same token on submission\nif (!verifyCsrfToken(body._csrf)) { ... }\n",[92,255,253],{"__ignoreMap":94},[77,257,259],{"id":258},"error-rate-limiter-not-working-in-production","Error: Rate limiter not working in production",[85,261,264],{"className":262,"code":263,"language":90},[88],"// Problem: In-memory rate limiter doesn't work with multiple instances\n\n// Solution: Use Redis or a database for distributed rate limiting\n// See the Redis example in Step 6 above\n\n// Or use a service like Upstash Redis\nimport { Ratelimit } from '@upstash/ratelimit';\nimport { Redis } from '@upstash/redis';\n\nconst ratelimit = new Ratelimit({\n  redis: Redis.fromEnv(),\n  limiter: Ratelimit.slidingWindow(5, '1 m'),\n});\n",[92,265,263],{"__ignoreMap":94},[77,267,269],{"id":268},"error-honeypot-field-visible-to-users","Error: Honeypot field visible to users",[85,271,274],{"className":272,"code":273,"language":90},[88],"// Problem: CSS not hiding the honeypot properly\n\n// Solution: Use multiple hiding techniques\n\u003Cdiv\n  style={{\n    position: 'absolute',\n    left: '-9999px',\n    top: '-9999px',\n    opacity: 0,\n    height: 0,\n    overflow: 'hidden',\n  }}\n  aria-hidden=\"true\"\n>\n  \u003Cinput tabIndex={-1} autoComplete=\"off\" {...register('website')} />\n\u003C/div>\n",[92,275,273],{"__ignoreMap":94},[277,278,279],"faq-section",{},[18,280,281],{},"Frequently Asked Questions",[283,284,286],"faq-item",{"question":285},"Do I need CSRF tokens if I'm using fetch/AJAX?",[18,287,288],{},"If you're using modern fetch with SameSite cookies and only accepting JSON content-type, CSRF protection is largely handled automatically. However, adding CSRF tokens provides defense-in-depth and supports older browsers.",[283,290,292],{"question":291},"Should I show validation errors to users?",[18,293,294],{},"Show friendly validation errors for legitimate mistakes (invalid email format, required fields). For security-related rejections (rate limits, honeypot triggers), return generic success to avoid tipping off attackers.",[283,296,298],{"question":297},"How do I handle file uploads in forms?",[18,299,300,301,306],{},"File uploads need additional security measures. See our ",[302,303,305],"a",{"href":304},"/blog/how-to/file-upload-security","File Upload Security guide"," for validation, malware scanning, and secure storage.",[283,308,310],{"question":309},"Is reCAPTCHA better than honeypots?",[18,311,312],{},"Honeypots are simpler and don't hurt UX. reCAPTCHA is more robust but adds friction. Consider using both: honeypots for basic bot blocking, reCAPTCHA for high-value forms or when honeypots aren't enough.",[283,314,316],{"question":315},"Should I validate on blur or submit?",[18,317,318],{},"Validate on blur for immediate feedback on individual fields. Always validate the full form on submit. Server-side validation happens on submit regardless of client-side choices.",[320,321,322,328,333],"related-articles",{},[323,324],"related-card",{"description":325,"href":326,"title":327},"Deep dive into Zod schema validation","/blog/how-to/zod-validation","Validate Input with Zod",[323,329],{"description":330,"href":331,"title":332},"Complete CSRF protection guide","/blog/how-to/implement-csrf-protection","Implement CSRF Protection",[323,334],{"description":335,"href":336,"title":337},"Protect your APIs from abuse","/blog/how-to/implement-rate-limiting","Implement Rate Limiting",{"title":94,"searchDepth":339,"depth":339,"links":340},2,[341,342,343,352,353],{"id":33,"depth":339,"text":34},{"id":53,"depth":339,"text":54},{"id":70,"depth":339,"text":71,"children":344},[345,347,348,349,350,351],{"id":79,"depth":346,"text":80},3,{"id":100,"depth":346,"text":101},{"id":116,"depth":346,"text":117},{"id":132,"depth":346,"text":133},{"id":148,"depth":346,"text":149},{"id":164,"depth":346,"text":165},{"id":215,"depth":339,"text":216},{"id":234,"depth":339,"text":235,"children":354},[355,356,357,358],{"id":238,"depth":346,"text":239},{"id":248,"depth":346,"text":249},{"id":258,"depth":346,"text":259},{"id":268,"depth":346,"text":269},"how-to","2026-01-13","Step-by-step guide to secure form validation. Client and server-side validation, CSRF protection, honeypots for bot detection, and security best practices.",false,"md",null,"yellow",{},true,"Complete guide to form security. Validation, CSRF tokens, honeypots, and rate limiting.","/blog/how-to/form-validation","[object Object]","HowTo",{"title":5,"description":361},{"loc":369},"blog/how-to/form-validation",[],"summary_large_image","i8uAwT2WHufX8ouSI_dZAz5wsPEsZ3aBXwtegsz4_SQ",1775843928420]