Skip to main content

Runtime Validation with Zod

Step 1: Define Strict Zod Schemas with Type Inference

Establish a single source of truth for API payloads. Zod’s static type inference bridges compile-time TypeScript checks and Schema Design & Validation Patterns at runtime. Define schemas using z.object() chained with .strict() to reject unknown keys. This enforces strict contract compliance and prevents silent payload drift. Export both the schema and the inferred type to maintain synchronization across microservices.

import { z } from 'zod';

export const CreateUserSchema = z.object({
 email: z.string().email(),
 role: z.enum(['admin', 'user']),
 metadata: z.record(z.string(), z.unknown()).optional()
}).strict();

export type CreateUserInput = z.infer<typeof CreateUserSchema>;

Step 2: Implement Request Interception Middleware

Integrate validation into the routing layer to intercept payloads before business logic execution. For framework-specific wiring, reference Implementing Zod validation in Express.js routes for exact middleware signatures and request lifecycle hooks. The middleware must parse the body, execute .safeParseAsync(), and halt execution on failure. This architecture prevents malformed data from reaching service handlers and isolates validation concerns.

import type { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';

export const validateRequest = (schema: ZodSchema) => {
 return async (req: Request, res: Response, next: NextFunction) => {
 const result = await schema.safeParseAsync(req.body);
 if (!result.success) {
 req.validationError = result.error.flatten();
 return next(new Error('VALIDATION_FAILED'));
 }
 req.body = result.data;
 next();
 };
};

Step 3: Configure CI/CD Contract Gating

Automate schema drift detection by embedding validation checks directly into the CI pipeline. Execute contract tests against live or mocked endpoints using a standard test runner. Configure the pipeline to block merges if runtime validation fails or if inferred types diverge from OpenAPI specifications. This gating mechanism ensures deployment consistency and prevents breaking changes from propagating to production environments.

name: Contract Validation
on:
 pull_request:
 branches: [main]
jobs:
 validate-contracts:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '20'
 cache: 'npm'
 - run: npm ci
 - run: npm run test:contracts
 - run: npm run typecheck
 env:
 NODE_ENV: test
 STRICT_SCHEMA_CHECK: true

Step 4: Standardize Validation Error Responses

Transform Zod’s native error structure into a predictable API contract. Extract field paths, machine-readable codes, and human-readable messages. Map these outputs to a centralized error handler to return structured JSON payloads. This guarantees clients receive actionable feedback without exposing internal stack traces. Align this output format with Designing Robust Error Response Contracts for cross-service consistency.

import { z } from 'zod';

export const formatZodError = (error: z.ZodError) => {
 const { fieldErrors } = error.flatten();
 return {
 status: 400,
 code: 'INVALID_PAYLOAD',
 details: Object.entries(fieldErrors).map(([field, messages]) => ({
 field,
 issues: messages
 }))
 };
};

Common Pitfalls & Validation Rules

Avoid implementation traps that degrade throughput or break API contracts. When migrating from Joi and Yup for Legacy Systems, recognize that Zod requires explicit .nullable() and .optional() chaining. It does not perform implicit type coercion. Enforce strict validation rules across the stack: never deploy .passthrough() in production endpoints. Validate query parameters independently from request bodies. Cache compiled schemas in high-throughput routes to minimize garbage collection overhead.

Troubleshooting Matrix

Issue Diagnostic Resolution
Type mismatch between inferred type and actual payload Check for .passthrough() or missing .strict() modifiers. Verify req.body mutation prior to validation. Apply .strict() to all root schemas. Run tsc --noEmit to catch inference breaks.
High latency on large payload validation Synchronous parsing blocks the event loop on payloads exceeding 50KB. Switch to .safeParseAsync(). Implement chunked validation or streaming parsers for bulk operations.
CI pipeline fails on type divergence Schema updates desynchronized from OpenAPI generation or frontend type definitions. Add a pre-commit hook executing zod-to-ts and openapi-typescript to enforce bidirectional contract sync.