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>
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
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: {} });
}
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>
);
}
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)