Integrating Zod for Form Validation in Next.js/Remix: A Quick Guide for Laravel Developers

Integrating Zod for Form Validation in Next.js/Remix: A Quick Guide for Laravel Developers
Photo by Hal Gatewood / Unsplash

Laravel is an excellent framework that offers all the tools necessary for building comprehensive web applications. One common feature encountered during web development is forms—a user interface allowing users to input data.

Before storing data into a database, it's essential to validate the input to match the column types in your database. Fortunately, Laravel comes with built-in validation that covers various data types, logic, and allows developers to customize error messages.

Since I'm currently learning Javascript frameworks like Next.js and Remix, luckily there are similar packages called Zod, TypeScript-first schema validation with static type inference.

How it works is very similar to what I'm used to in Laravel. 

Writing schema (validation logic)

After installing Zod, the initial step is to define a schema or validation logic for each input value. Similar to Laravel's built-in validation, Zod provides numerous data types and allows you to chain multiple schemas or logic simultaneously.

For instance, here's how you can define that the input named 'title' must be a string with a minimum length of 10 characters. The second block is from Laravel documentation.

const FormSchema = z.object({
    title: z.string().min(10),
})

const validatedFields = FormSchema.safeParse({
  title: formData.get("title") as string,
})
public function store(Request $request): RedirectResponse
{
    $validated = $request->validate([
        'title' => 'required|string|max:10',
    ]);
}

Displaying the validation errors

If the incoming form field does not pass the schema or validation rules, you must return the error and format it into a user-readable message.

Fortunately, Laravel automatically redirects the user to the previous location, stores the error and form fields in the session. This feature is very helpful because it eliminates the need for users to retype their input, and developers can utilize the session to display error messages on the frontend.

In the latest Laravel 11 release, the functionality is even better as it can now return the error format in JSON. This makes it easier to parse, especially if you're using reactive frameworks like React or Vue.js for your frontend.

{
    "message": "The team name must be a string. (and 4 more errors)",
    "errors": {
        "team_name": [
            "The team name must be a string.",
            "The team name must be at least 1 characters."
        ],
        "authorization.role": [
            "The selected authorization.role is invalid."
        ],
    }
}

On the other hand, the error messages produced by Zod can be a bit tricky to handle. Here's a quick example of how you can format the error messages similarly to the Laravel example above.

// Define the type
export type State = {
    errors? : {
        title? : string[],
    }
    message? : string | null
}

export async function createBlogPost(prevState: State, formData: FormData) {
      // Validate form using Zod
    const validatedFields = FormSchema.safeParse({
        title: formData.get("title") as string,
        description: formData.get("description") as string,
    })

    // If form validation fails, return errors early. Otherwise, continue.
    if (!validatedFields.success) {
        return {
            errors: validatedFields.error.flatten().fieldErrors,
            message: "Sorry something went wrong.",
        }
    }
}