Validating Runtime Data with Zod and TypeScript

A
Author
08 Jul, 2026 1 min read 1.18K views

TypeScript checks your code, not your data. A JSON response, a form submission or an environment variable can be anything at runtime. Zod validates data against a schema and gives you the TypeScript type from that same schema.

Define once, get both

import { z } from 'zod';

const User = z.object({
  id: z.number().int(),
  name: z.string().min(1),
  email: z.email(),
  role: z.enum(['admin', 'member']),
  createdAt: z.coerce.date(),
});

type User = z.infer<typeof User>; // the TypeScript type, no duplication

Validate API responses

async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return User.parse(await res.json()); // throws a ZodError if the shape is wrong
}

Handle errors without exceptions

const result = User.safeParse(body);
if (!result.success) {
  return Response.json({ errors: z.flattenError(result.error).fieldErrors }, { status: 422 });
}
const user = result.data; // typed User

Validate environment variables at startup

const Env = z.object({
  DATABASE_URL: z.url(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'test', 'production']),
});

export const env = Env.parse(process.env); // crash early with a clear message

Useful patterns

  • .optional(), .nullable() and .default() for missing values.
  • .transform() to normalise data (trim, lowercase) during parsing.
  • .refine() for custom rules such as "end date after start date".
  • User.partial() for update payloads, User.omit({ id: true }) for create payloads.

The examples use the Zod 4 API. Zod 3 code like z.string().email() still works but the top-level forms such as z.email() are preferred.

Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.