DEV Community

Alex Spinov
Alex Spinov

Posted on

Zod Has a Free API — TypeScript Schema Validation in 11KB

Zod is the TypeScript-first schema validation library with 34K+ GitHub stars — it validates data at runtime AND infers TypeScript types automatically. Zero dependencies, 11KB.

Why Zod?

  • TypeScript-first — define schema once, get types automatically
  • Zero dependencies — runs anywhere JavaScript runs
  • Composable — combine, extend, transform schemas
  • Error messages — detailed, customizable error reporting
  • Ecosystem — React Hook Form, tRPC, Drizzle, Hono all use Zod

Quick Start

npm install zod
Enter fullscreen mode Exit fullscreen mode
import { z } from "zod";

// Define a schema
const UserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().min(18).max(120),
  role: z.enum(["admin", "user", "moderator"]),
});

// Infer TypeScript type (no manual type definition!)
type User = z.infer<typeof UserSchema>;
// type User = { name: string; email: string; age: number; role: "admin" | "user" | "moderator" }

// Validate data
const result = UserSchema.safeParse({
  name: "Alice",
  email: "alice@example.com",
  age: 30,
  role: "admin",
});

if (result.success) {
  console.log(result.data); // Fully typed as User
} else {
  console.log(result.error.issues); // Detailed error info
}
Enter fullscreen mode Exit fullscreen mode

Schema Types

// Primitives
const str = z.string();
const num = z.number();
const bool = z.boolean();
const date = z.date();
const bigint = z.bigint();

// String validations
const email = z.string().email();
const url = z.string().url();
const uuid = z.string().uuid();
const minMax = z.string().min(5).max(100);
const regex = z.string().regex(/^[a-z]+$/);
const trim = z.string().trim().toLowerCase();

// Number validations
const positive = z.number().positive();
const integer = z.number().int();
const range = z.number().min(0).max(100);

// Arrays
const tags = z.array(z.string()).min(1).max(10);
const uniqueIds = z.array(z.number()).nonempty();

// Enums
const status = z.enum(["active", "inactive", "banned"]);

// Unions
const response = z.union([
  z.object({ status: z.literal("success"), data: z.any() }),
  z.object({ status: z.literal("error"), message: z.string() }),
]);

// Discriminated union (faster)
const event = z.discriminatedUnion("type", [
  z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
  z.object({ type: z.literal("scroll"), offset: z.number() }),
  z.object({ type: z.literal("keypress"), key: z.string() }),
]);
Enter fullscreen mode Exit fullscreen mode

Transforms (Parse + Transform)

const DateString = z.string().transform((val) => new Date(val));
// Input: string → Output: Date

const CurrencyAmount = z.string()
  .regex(/^\$[\d,]+\.?\d*$/)
  .transform((val) => parseFloat(val.replace(/[$,]/g, "")));
// Input: "$1,234.56" → Output: 1234.56

const SearchParams = z.object({
  page: z.coerce.number().default(1),
  limit: z.coerce.number().default(10),
  sort: z.enum(["asc", "desc"]).default("desc"),
  q: z.string().optional(),
});

// Coerce converts URL params (strings) to proper types
const params = SearchParams.parse({
  page: "3",    // string → number 3
  limit: "25",  // string → number 25
});
Enter fullscreen mode Exit fullscreen mode

Express.js Validation Middleware

import express from "express";
import { z, ZodError } from "zod";

function validate(schema: z.ZodSchema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        errors: result.error.issues.map((i) => ({
          field: i.path.join("."),
          message: i.message,
        })),
      });
    }
    req.body = result.data;
    next();
  };
}

const CreateOrderSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive(),
  })).nonempty(),
  shipping: z.object({
    address: z.string().min(5),
    city: z.string(),
    zip: z.string().regex(/^\d{5}$/),
  }),
  couponCode: z.string().optional(),
});

app.post("/orders", validate(CreateOrderSchema), (req, res) => {
  // req.body is fully validated and typed
  const order = createOrder(req.body);
  res.status(201).json(order);
});
Enter fullscreen mode Exit fullscreen mode

React Hook Form Integration

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

const SignupSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
  password: z.string().min(8).regex(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
    "Must contain uppercase, lowercase, and number"
  ),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

type SignupForm = z.infer<typeof SignupSchema>;

function SignupPage() {
  const { register, handleSubmit, formState: { errors } } = useForm<SignupForm>({
    resolver: zodResolver(SignupSchema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("username")} />
      {errors.username && <span>{errors.username.message}</span>}

      <input {...register("email")} type="email" />
      {errors.email && <span>{errors.email.message}</span>}

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

      <input {...register("confirmPassword")} type="password" />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

      <button type="submit">Sign Up</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables Validation

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(20),
  PORT: z.coerce.number().default(3000),
  REDIS_URL: z.string().url().optional(),
  SMTP_HOST: z.string().optional(),
  SMTP_PORT: z.coerce.number().optional(),
});

// Validate at startup — fail fast if env is wrong
export const env = EnvSchema.parse(process.env);
Enter fullscreen mode Exit fullscreen mode

Zod vs Joi vs Yup vs Valibot

Feature Zod Joi Yup Valibot
Size 11KB 50KB+ 25KB 1KB
TypeScript First-class @types Built-in First-class
Type inference Automatic No Partial Automatic
Tree-shakeable No No No Yes
Transforms Yes Yes Yes Yes
Async validation Yes Yes Yes Yes
Ecosystem Huge Large Large Growing

Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.

Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.

Top comments (0)