Form Validation: I Tried Every Approach. Then I Built StaticForm's Simple Solution.

From Zod schemas to HTML5 validation to custom JavaScript, I've implemented form validation every possible way. Most approaches are overkill. Here's what I built instead.

StaticForm Team

I spent three days building the perfect form validation system. Type-safe schemas. Beautiful error messages. Client-side and server-side validation. Real-time feedback as users typed. The works. Then I watched a user struggle with it for five minutes before giving up.

The TypeScript Validation Rabbit Hole

It started with Zod. Everyone was using Zod. Type-safe schemas, automatic validation, works on client and server. Perfect:

import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  phone: z.string().regex(/^[0-9]{10}$/, 'Phone must be 10 digits'),
  message: z.string().min(10, 'Message too short'),
});

Elegant. Clean. Typed. I felt like a real engineer. Then I had to actually use it:

function validateForm(data: unknown) {
  try {
    return contactSchema.parse(data);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { errors: formatErrors(error) };
    }
  }
}

function formatErrors(error: z.ZodError) {
  // 20 more lines of error formatting...
}

function displayErrors(errors: Record<string, string>) {
  // 30 more lines of DOM manipulation...
}

// And the same thing on the server side...
// And keeping both in sync...
// And handling edge cases...

What started as 10 lines of schema turned into 200 lines of validation logic, error handling, and UI updates. And you know what? Users still managed to submit invalid data. They disabled JavaScript. They used browser autofill that bypassed validation. They found edge cases I didn’t handle.

HTML5 Validation: Simple But Broken

Okay, I thought. Maybe I’m overthinking this. HTML5 has built-in validation:

<input type="email" required>
<input type="tel" pattern="[0-9]{10}">

Simple. No JavaScript. Works everywhere. Except it doesn’t work everywhere. Browser support is inconsistent. Error messages look different in Chrome vs Firefox vs Safari. You can’t style them properly. Users can easily bypass them. And if someone has JavaScript disabled (rare but happens), your server still needs validation. So you’re validating twice. But the HTML5 validation and server validation can get out of sync.

I shipped a form with HTML5 validation. Got an angry email two days later: “Your form won’t accept my phone number!” Their number had dashes. Like this: 555-123-4567. Normal formatting. But my regex only accepted digits. The error message said “Phone must be 10 digits” which made no sense to them because their phone number IS 10 digits. It just has dashes.

Custom JavaScript: The Maintenance Nightmare

Fine, I’ll write my own validation. Full control. Perfect error messages. Custom logic for every field:

function validateEmail(email) {
  if (!email.includes('@')) {
    return 'Invalid email address';
  }
  if (email.split('@')[1].split('.').length < 2) {
    return 'Email must have a valid domain';
  }
  // ... 20 more edge cases
}

function validatePhone(phone) {
  const cleaned = phone.replace(/[^0-9]/g, '');
  if (cleaned.length !== 10) {
    return 'Phone must be 10 digits';
  }
  // ... but also handle international numbers
  // ... and various formats
  // ... and country codes
}

// ... repeat for every field

This worked! Until it didn’t. Users found edge cases. International users had different phone formats. Email validation is actually really complicated. Date formats vary by locale. Every week I was adding new validation rules. Fixing bugs. Handling edge cases. The validation code became a maintenance nightmare.

What Users Actually Need

Here’s what I realized after all this: users don’t need perfect validation. They need helpful validation. If they make a mistake, tell them quickly and clearly. Don’t make them guess what’s wrong. Don’t use technical jargon. Don’t be pedantic about formats.

The best validation I ever built was the simplest: required fields must be filled, email must look like an email, phone must have enough digits, message can’t be empty. That’s it. No complex regex. No type-safe schemas. No elaborate error objects. And you know what? It worked better than the complicated versions. Because it was clear what was required and the error messages made sense.

Why I Built StaticForm’s Validation System

When I built StaticForm, I made validation simple. Really simple. Click on a field. Choose validation rules. Done. Want email validation? One click. Want a minimum length? Type it in. The UI literally says “Minimum length: [ ]” and you type a number. That’s it. No schemas. No code. No “how do I configure this?” questions.

And it validates on the server, so it can’t be bypassed. Even if users disable JavaScript or manipulate the DOM or submit via curl, the validation still happens. I moved five forms to StaticForm. Total time spent on validation: about 10 minutes. Total validation code written: zero lines.

What StaticForm Actually Supports

StaticForm keeps validation simple and focused on what most forms actually need. For text fields: required flag, minimum length, maximum length, and email validation. For number fields: required flag, number validation (just numbers), positive numbers only, or number ranges. That’s it. No custom regex. No conditional validation. No complex business logic.

But honestly? 95% of forms don’t need complex validation. They need simple, clear rules that make sense to users. Email validation, required fields, length limits. That covers almost everything.

The Mistake I Made

I was building validation for me, not for users. Type safety is great for developers. But users don’t care about type safety. They care about filling out the form and getting a response. Complex validation schemas make developers feel good. But they don’t make forms better. They just make them harder to maintain.

The best validation is invisible. It catches obvious mistakes. It provides clear feedback. It doesn’t get in the way. StaticForm’s validation isn’t fancy. It’s just clear and simple. Which is exactly what it should be.

Get 10 free credits to test your form at app.staticform.app. Pay as you go, or buy a plan to save money.