LogoStarterkitpro
Walkthrough

Schema Validation

Validate data in your API routes and server actions using Zod

Schema Validation with Zod

Data validation is crucial for ensuring the integrity and security of your application. While TypeScript provides type checking at compile time, it doesn't offer runtime validation for data coming from external sources like API requests. This is where schema validation libraries like Zod come in.

Runtime Validation: TypeScript types are removed during compilation and don't provide any runtime validation. When your API or server action is called from external sources, you need runtime validation to ensure data correctness and security.

Why Use Zod?

Zod is a TypeScript-first schema validation library that allows you to:

  1. Define schemas that validate your data at runtime
  2. Generate TypeScript types from your schemas
  3. Handle validation errors in a structured way
  4. Transform and parse data as needed

Setting Up Zod Schemas

First, create your validation schemas in a dedicated location:

lib/validation-schemas
import { z } from "zod";
 
export const leadFormSchema = z.object({
  email: z.string().email("Please enter a valid email address"),
});
 
// Export the inferred type for use in TypeScript
export type LeadFormSchema = z.infer<typeof leadFormSchema>;

Using safeParse for Validation

The safeParse method is the recommended way to validate data. Unlike parse, which throws an error on invalid data, safeParse returns an object with a success flag and either the validated data or error information.

actions/lead-actions.ts
"use server";
import {
  LeadFormSchema,
  leadFormSchema,
} from "@/lib/validation-schemas/lead-validations";
import { createLead } from "@/queries/lead-queries";
 
export const generateLead = async (data: LeadFormSchema) => {
  try {
    // Validate the incoming data
    const validationResult = leadFormSchema.safeParse(data);
    
    // Handle validation failure
    if (!validationResult.success) {
      return {
        status: "error",
        errors: validationResult.error.flatten(),
      };
    }
 
    // Use the validated data with type safety
    const validatedData = validationResult.data;
    await createLead(validatedData.email);
 
    return { status: "success" };
  } catch (error) {
    console.error("Error creating lead:", error);
    return { status: "error" };
  }
};

Understanding the Error Structure

When validation fails, safeParse returns an object with success: false and an error property. The flatten() method creates a user-friendly structure:

{
  status: "error",
  errors: {
    // Field-specific errors
    fieldErrors: {
      email: ["Please enter a valid email address"],
      name: ["Name must be at least 2 characters"]
    },
    // Form-level errors (if any)
    formErrors: []
  }
}

This structure makes it easy to:

  • Identify which fields have errors
  • Display specific error messages for each field
  • Handle form-level errors separately

Client-Side Error Handling

components/lead-form.tsx
"use client";
 
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  LeadFormSchema,
  leadFormSchema,
} from "@/lib/validation-schemas/lead-validations";
 
// pass the schema and the inferred type for use in TypeScript
 
const form = useForm<LeadFormSchema>({
  resolver: zodResolver(leadFormSchema),
  defaultValues: {
    email: "",
  },
});
 
const {
  handleSubmit,
  formState: { isSubmitting },
} = form;
 
const handleSubmit = async (data) => {
  const result = await serverAction(data);
 
  if (result.status === "success") {
    toast.success("Success message");
    form.reset();
  } else if (result.errors?.fieldErrors) {
    // Approach 1:
    // Map server errors to form fields and these wil be treated like react hook form errors
    Object.entries(result.errors.fieldErrors).forEach(([field, errors]) => {
      if (errors?.[0]) {
        form.setError(field as any, {
          type: "server",
          message: errors[0],
        });
      }
    });
    // Approach 2:
    // Display all validation errors in toaster
    // Object.values(result.errors.fieldErrors || {})
    //   .flat()
    //   .forEach((error) => error && toast.error(error));
  }
};

Best Practices

  1. Consistent Error Handling: Use the same pattern for handling validation errors across your application.

  2. Separate Validation Schemas: Keep your validation schemas in a dedicated directory for better organization.

  3. Type Inference: Use Zod's type inference to ensure your TypeScript types match your validation schemas.

  4. Custom Error Messages: Provide clear, user-friendly error messages in your schemas.

Conclusion

Schema validation with Zod provides robust runtime validation for your Next.js application. By using safeParse and properly handling validation errors, you can create a secure and user-friendly experience that catches data issues before they cause problems.

On this page