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"],
});

Generate a Zod schema from your JSON data automatically?

Use the JSON to Zod Converter →