May 9, 2026 · 7 min read

Zod in Production: Runtime Type Safety Without the Tax

TypeScript gives you confidence at compile time. But your API receives strings and JSON — the types are gone by the time a request lands. Zod validates and coerces at the request boundary so malformed data never reaches your business logic.

Write the Schema First

The key pattern: write the Zod schema first, derive the TypeScript type from it with z.infer. One definition, zero drift between the runtime validator and the compile-time type.

import { z } from 'zod';

const CreateUserSchema = z.object({
  email:    z.string().email(),
  name:     z.string().min(1).max(100),
  role:     z.enum(['admin', 'user']).default('user'),
  age:      z.number().int().min(18).optional(),
});

type CreateUser = z.infer<typeof CreateUserSchema>;
//   ^-- { email: string; name: string; role: "admin" | "user"; age?: number }

One schema. The type is derived from it automatically. You cannot have a mismatch.

Middleware, Not Handlers

Wrap validation in middleware instead of calling .parse() in every handler. Use safeParse — not parse — in async contexts, since parse throws and async route handlers won't always catch it through your error middleware.

function validate<T>(schema: z.ZodSchema<T>) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        errors: result.error.flatten().fieldErrors,
      });
    }
    req.body = result.data;   // coerced and typed
    next();
  };
}

router.post('/users', validate(CreateUserSchema), createUserHandler);

Now every handler downstream of that middleware receives validated, coerced data. No if (!email) guards, no string-to-number conversions scattered across the codebase.

The Overhead Question

Validation error types
Fig 1. Error distribution: type mismatches dominate at 42%.
Zod parse overhead by payload size
Fig 2. Parse overhead: 0.4μs small payloads, 6.8μs large. Our p99 is 380ms — Zod adds under 2%.

Parse overhead at our scale: 0.4μs for small payloads, 6.8μs for large ones. Our request p99 latency is 380ms. Zod adds less than 2%. It is not a performance question.

What it is is a correctness question. We caught 340 malformed requests in the first week after adding Zod middleware to a service that had been running for two years. They were all silently succeeding before — doing the wrong thing with garbage data.

What Not to Validate

Don't validate internal function calls, database results you wrote the schema for, or anything that's already been validated earlier in the request lifecycle. Validate at system boundaries: incoming HTTP requests, webhook payloads, external API responses. Nowhere else.

Related topics
TypeScriptBackend

T
Tanmay Bohra
Full Stack Engineer at Grant Thornton Bharat. Building high-concurrency systems in Go and TypeScript.
← portfolio chat with tanmay ↗