DEV Community

Alex Spinov
Alex Spinov

Posted on

Zod Has a Free API — Heres How to Validate Everything in TypeScript

Zod is a TypeScript-first schema validation library. Define a schema once and get runtime validation, static types, and transformation — all with zero dependencies.

Why Zod?

  • TypeScript-first: Schemas ARE your types
  • Zero dependencies: 52KB bundle
  • Composable: Chain, merge, extend schemas
  • Ecosystem: Works with tRPC, React Hook Form, Hono, Astro
  • Error messages: Human-readable by default
  • Transforms: Parse and transform in one step

Install

npm install zod
Enter fullscreen mode Exit fullscreen mode

Basic Schemas

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'moderator']),
  tags: z.array(z.string()).default([]),
});

// Infer TypeScript type from schema
type User = z.infer<typeof userSchema>;
// { name: string; email: string; age?: number; role: 'admin' | 'user' | 'moderator'; tags: string[] }

// Validate
const result = userSchema.safeParse(input);
if (result.success) {
  console.log(result.data); // Typed as User
} else {
  console.log(result.error.issues); // Detailed errors
}
Enter fullscreen mode Exit fullscreen mode

Transformations

const priceSchema = z.string()
  .transform(val => parseFloat(val))
  .pipe(z.number().positive());

priceSchema.parse('29.99'); // 29.99 (number)
priceSchema.parse('-5');    // throws: Number must be positive
Enter fullscreen mode Exit fullscreen mode

API Request Validation

const createPostSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  body: z.string().min(10),
  published: z.boolean().default(false),
  tags: z.array(z.string()).max(5).optional(),
});

// Express middleware
app.post('/api/posts', (req, res) => {
  const result = createPostSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() });
  }
  // result.data is fully typed
  const post = await db.post.create({ data: result.data });
  res.json(post);
});
Enter fullscreen mode Exit fullscreen mode

Discriminated Unions

const eventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('scroll'), position: z.number() }),
  z.object({ type: z.literal('input'), value: z.string(), field: z.string() }),
]);
Enter fullscreen mode Exit fullscreen mode

Environment Variables

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.string().transform(Number).pipe(z.number().int().positive()),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  API_KEY: z.string().min(32),
});

export const env = envSchema.parse(process.env);
Enter fullscreen mode Exit fullscreen mode

Recursive Schemas

type Category = {
  name: string;
  children: Category[];
};

const categorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(categorySchema),
  })
);
Enter fullscreen mode Exit fullscreen mode

Real-World Use Case

A team added Zod to their Express API. In the first week, input validation caught 23 malformed requests that had been silently causing database errors. They also eliminated 400 lines of manual validation code.


Need to automate data collection? Check out my Apify actors for ready-made scrapers, or email spinov001@gmail.com for custom solutions.

Top comments (0)