DEV Community

Rafał Goławski
Rafał Goławski

Posted on

Form Validation in Remix with Zod 🔐

Introduction

In this guide, I'll show you how to implement form validation in a Remix application using Zod - a TypeScript-first schema validation library. If you're working with Remix and need a robust validation solution, this tutorial is for you.

Understanding the Basics

Remix is a full-stack React framework that has revolutionized how we handle server-client interactions. Originally developed by the React Router team and now part of React Router v7. Before diving into the implementation, it's important to understand that Remix uses two core concepts: loaders (for data loading) and actions (for data writes). Both operate on the server side, giving us a secure environment for data validation.

Building the Form

Let's start with a simple sign-in form using Remix's <Form> component. The key difference between Remix's <Form> and a traditional HTML form is that Remix handles the routing on the client side, providing a smooth, single-page application experience without full page reloads.

// SignInForm.tsx
import { Form } from "@remix-run/react";

<Form method="POST">
  {/* Email field container */}
  <div className="form-group">
    <label htmlFor="email">Email:</label>
    <input id="email" type="email" name="email" required />
  </div>

  {/* Password field container */}
  <div className="form-group">
    <label htmlFor="password">Password:</label>
    <input id="password" type="password" name="password" required />
  </div>

  <button type="submit">Sign in</button>
</Form>
Enter fullscreen mode Exit fullscreen mode

When a form is submitted, Remix will automatically match this POST request with the corresponding action function in our route file.

Setting Up Zod Validation

Before implementing validation, you'll need to install Zod. It's a TypeScript-first schema declaration and validation library that provides a robust way to validate your data with great type inference.

npm install zod
Enter fullscreen mode Exit fullscreen mode

Now, let's implement the action with Zod validation. This is where the real power of server-side validation comes into play:

// SignInForm.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { z } from 'zod'

// Define the validation schema using Zod
const schema = z.object({
  email: z.string()
    .nonempty('Email is required') // Checks if the field is not empty
    .email('Email is not valid'),  // Validates email format
  password: z.string()
    .min(12, 'Password must be 12 or more characters long') // Enforces password length
  });

export async function action({ request }: ActionFunctionArgs) {
  // Extract form data from the request
  const formData = await request.formData();

  // Attempt to validate the form data
  // safeParse returns an object with either success or error information
  const result = schema.safeParse({
    email: formData.get('email'), // Extract email from form data
    password: formData.get('password') // Extract password from form data
  });

  // Handle validation result
  if (!result.success) {
    // If validation fails, return the formatted error messages
    return json({ fieldErrors: result.error.flatten().fieldErrors })
  }

  // At this point, validation has passed
  // You can proceed with your business logic (e.g., user authentication)
  return json({ fieldErrors: {} });
}
Enter fullscreen mode Exit fullscreen mode

Implementing Error Display

The final step is displaying validation errors to users. This implementation shows how to properly handle and display error messages while dealing with potential undefined values safely:

// SignInForm.tsx
import { Form, useActionData } from "@remix-run/react";

export default function SignInForm() {
  // Get the action data which contains our validation results
  const actionData = useActionData<typeof action>();

  // Safely extract error messages with fallbacks
  const errors = {
    email: actionData?.fieldErrors?.email?.[0] ?? '', 
    password: actionData?.fieldErrors?.password?.[0] ?? '',
  };

  return (
    <Form method="POST">
      {/* Email field container */}
      <div className="form-group">
        <label htmlFor="email">Email:</label>
        <input id="email" type="email" name="email" required />
        {/* Display email error */}
        {errors.email && <p className="error">{errors.email}</p>}
      </div>

      {/* Password field container */}
      <div className="form-group">
        <label htmlFor="password">Password:</label>
        <input id="password" type="password" name="password" required />
        {/* Display password error */}
        {errors.password && <p className="error">{errors.password}</p>}
      </div>

      <button type="submit">Sign in</button>
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

While it might seem like extra work to handle Zod's error structure, in my opinion the benefits of type safety and reliable validation outweigh the initial setup complexity.

Wrapping Up

That would be it for this tutorial - I hope you've learned something new. If you'd like to get a better grasp of it, I also recommend checking out the form validation guide from the official Remix docs. If you feel like it's too much and would prefer a ready-to-go solution instead, you can choose Conform or RVF - they're both great. Let me know what you think.

Thanks for reading!

Top comments (0)