Special Sponsor:PromptBuilder— Fast, consistent prompt creation powered by 1,000+ expert templates.
Make your Product visible here.Contact Us

How to Use Zod with React

Practical guide to Zod in React apps — form validation, React Hook Form integration, API response validation, and TypeScript type inference.

1. Install Zod

npm install zod
# or
yarn add zod

2. Define a Schema and Infer the TypeScript Type

Create a schema once — Zod infers the TypeScript type automatically:

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  age: z.number().int().min(18, "Must be 18 or older").optional(),
});

// TypeScript type inferred from schema
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number }

3. Validate Data with safeParse

Use safeParse to validate without throwing — returns a success/error result:

const data = { name: "Alice", email: "alice@example.com" };

const result = UserSchema.safeParse(data);

if (result.success) {
  // result.data is fully typed as User
  console.log(result.data.name);
} else {
  // result.error.flatten() gives field-level errors
  const errors = result.error.flatten();
  console.log(errors.fieldErrors);
  // { email: ["Invalid email address"] }
}

4. Validate API Responses in React

Parse API responses to ensure they match your expected shape:

import { useState, useEffect } from 'react';
import { z } from 'zod';

const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number(),
});

type Post = z.infer<typeof PostSchema>;

function usePost(id: number) {
  const [post, setPost] = useState<Post | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
      .then(r => r.json())
      .then(data => {
        const result = PostSchema.safeParse(data);
        if (result.success) {
          setPost(result.data);
        } else {
          setError("API response did not match expected shape");
          console.error(result.error.flatten());
        }
      });
  }, [id]);

  return { post, error };
}

5. React Hook Form + Zod (Recommended)

Install the resolver:

npm install react-hook-form @hookform/resolvers

Use the Zod resolver in your form:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const LoginSchema = z.object({
  email: z.string().email("Enter a valid email"),
  password: z.string().min(8, "Password must be at least 8 characters"),
});

type LoginData = z.infer<typeof LoginSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginData>({
    resolver: zodResolver(LoginSchema),
  });

  const onSubmit = (data: LoginData) => {
    // data is typed as LoginData — no manual type assertion needed
    console.log(data.email, data.password);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}

      <input {...register("password")} type="password" placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit">Login</button>
    </form>
  );
}

6. Environment Variable Validation

Validate environment variables at startup so missing vars fail fast:

import { z } from 'zod';

const EnvSchema = z.object({
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith("pk_"),
  DATABASE_URL: z.string().min(1),
});

// Throws at startup if env vars are missing or invalid
const env = EnvSchema.parse(process.env);

export default env;

7. Reusable Field Schemas

Extract common fields to avoid repetition:

import { z } from 'zod';

// Reusable building blocks
const emailField = z.string().email("Invalid email");
const passwordField = z.string()
  .min(8, "At least 8 characters")
  .regex(/[A-Z]/, "Must contain an uppercase letter")
  .regex(/[0-9]/, "Must contain a number");

const LoginSchema = z.object({
  email: emailField,
  password: passwordField,
});

const RegisterSchema = z.object({
  email: emailField,
  password: passwordField,
  confirmPassword: passwordField,
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords do not match",
  path: ["confirmPassword"],
});

Frequently Asked Questions

Do I need TypeScript to use Zod?

No, but Zod is most useful with TypeScript. The z.infer<typeof Schema> feature automatically derives TypeScript types from schemas, eliminating the need to write types and validation separately.

What is the difference between parse and safeParse in Zod?

parse() throws a ZodError if validation fails. safeParse() returns { success: true, data } or { success: false, error } — it never throws. Use safeParse() in most React code to handle errors gracefully without try/catch.

Is Zod or Yup better for React Hook Form?

Both integrate with React Hook Form via @hookform/resolvers. Zod is generally preferred for TypeScript projects because types are inferred from the schema automatically. Yup is the better choice if you use Formik or have an existing Yup codebase.

How do I show Zod validation errors in a React form?

Use React Hook Form with zodResolver — errors appear in formState.errors keyed by field name. Without React Hook Form, call safeParse() on submit and use result.error.flatten().fieldErrors to get field-level messages.

Can I use Zod to validate environment variables in Next.js?

Yes. Define a Zod schema for process.env and call schema.parse(process.env) at the top of a config file. This throws at startup if required variables are missing or invalid, catching config errors before they reach production.

From the blog

View all →

Generate a Zod schema from your JSON data automatically?

Use the JSON to Zod Converter →