Zod Showcase

Real-World Use Cases

Practical patterns for production use. Zod is particularly useful wherever untrusted data enters your app: forms, APIs, and environment configuration.

Form Validation

Validate user-submitted form data before processing it. Try setting age below 18 or password shorter than 8 characters.

Schema
import { z } from 'zod';

const SignupSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
  age: z.number().int().min(18),
});

// Usage in a form handler:
// const result = SignupSchema.safeParse(formData);
// if (!result.success) return errors(result.error);
Inferred Type
type Signup = {
  email: string;
  password: string;
  age: number;
}
Output
{
  "email": "alice@example.com",
  "password": "hunter42",
  "age": 25
}

API Response Parsing

Parse and validate data returned from an external API. If the API changes its shape, Zod catches it at the boundary. Try removing total or changing published to a string.

Schema
import { z } from 'zod';

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

const ApiResponseSchema = z.object({
  data: z.array(PostSchema),
  total: z.number(),
});

// Usage:
// const raw = await fetch('/api/posts').then(r => r.json());
// const response = ApiResponseSchema.parse(raw);
Inferred Type
type Post = {
  id: number;
  title: string;
  published: boolean;
};

type ApiResponse = {
  data: Post[];
  total: number;
}
Output
{
  "data": [
    {
      "id": 1,
      "title": "Getting started with Zod",
      "published": true
    },
    {
      "id": 2,
      "title": "Advanced patterns",
      "published": false
    }
  ],
  "total": 2
}

Discriminated Unions

z.discriminatedUnion() is faster and gives clearer errors than z.union() when all branches share a common literal field. Zod reads the discriminator to pick the right branch before validating the rest. Try switching type between "success" and "error" with the matching fields.

Schema
import { z } from 'zod';

const ApiResult = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('success'),
    data: z.object({ id: z.number(), name: z.string() }),
  }),
  z.object({
    type: z.literal('error'),
    message: z.string(),
    code: z.number(),
  }),
]);
Inferred Type
type ApiResult =
  | { type: 'success'; data: { id: number; name: string } }
  | { type: 'error'; message: string; code: number }
Output
{
  "type": "success",
  "data": {
    "id": 1,
    "name": "Alice"
  }
}

Transforms

.transform() runs after validation and reshapes the output. The Output panel shows the transformed result, not the raw input. Use it to normalise data: trim strings, coerce types, rename fields, or compute derived values. z.infer gives the output type. Use z.input<typeof schema> to get the pre-transform input type.

Schema
import { z } from 'zod';

const SearchSchema = z.object({
  // Trim whitespace and lowercase the query
  query: z.string().transform(s => s.trim().toLowerCase()),
  // Coerce the string "10" to the number 10
  limit: z.string().transform(Number),
});

// z.input<typeof SearchSchema>  → { query: string; limit: string }
// z.output<typeof SearchSchema> → { query: string; limit: number }
Inferred Type
// z.infer gives the OUTPUT type (after transform):
type Search = {
  query: string;
  limit: number;  // was string before transform
}
Output
{
  "query": "hello world",
  "limit": 10
}

Environment Variable Validation

Validate process.env at startup so your app crashes early with a clear error instead of mysteriously misbehaving at runtime. Try removing DATABASE_URL or setting NODE_ENV to an unknown value.

Schema
import { z } from 'zod';

const EnvSchema = z.object({
  DATABASE_URL: z.url(),
  PORT: z.string().regex(/^\d+$/),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});

// At app startup:
// const env = EnvSchema.parse(process.env);
Inferred Type
type Env = {
  DATABASE_URL: string; // z.url() infers string
  PORT: string;
  NODE_ENV: 'development' | 'production' | 'test';
}
Output
{
  "DATABASE_URL": "http://localhost:5432/mydb",
  "PORT": "3000",
  "NODE_ENV": "development"
}