import { z } from 'zod';
export const createUserSchema = z
.object({
email: z.string().email(),
name: z.string().min(1).max(120),
age: z.coerce.number().int().min(13).max(130).optional(),
role: z.enum(['member', 'admin']).default('member'),
})
.strict();
export const listUsersSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().trim().min(1).optional(),
});
export const userIdSchema = z.object({
id: z.string().uuid(),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type ListUsersQuery = z.infer<typeof listUsersSchema>;
import { RequestHandler } from 'express';
import { ZodSchema } from 'zod';
type Target = 'body' | 'query' | 'params';
export class ValidationError extends Error {
constructor(public fieldErrors: Record<string, string[] | undefined>) {
super('Request validation failed');
this.name = 'ValidationError';
}
}
export function validate(schema: ZodSchema, target: Target = 'body'): RequestHandler {
return (req, _res, next) => {
const result = schema.safeParse(req[target]);
if (!result.success) {
return next(new ValidationError(result.error.flatten().fieldErrors));
}
// Replace raw input with parsed, coerced, defaulted data.
(req as Record<Target, unknown>)[target] = result.data;
next();
};
}
import { Router, ErrorRequestHandler } from 'express';
import { validate, ValidationError } from './validate';
import {
createUserSchema,
listUsersSchema,
userIdSchema,
CreateUserInput,
ListUsersQuery,
} from './schemas';
import { userService } from './userService';
export const usersRouter = Router();
usersRouter.get('/', validate(listUsersSchema, 'query'), async (req, res) => {
const query = req.query as unknown as ListUsersQuery;
const users = await userService.list(query);
res.json({ data: users, page: query.page, limit: query.limit });
});
usersRouter.post('/', validate(createUserSchema), async (req, res) => {
const input = req.body as CreateUserInput;
const user = await userService.create(input);
res.status(201).json({ data: user });
});
usersRouter.get('/:id', validate(userIdSchema, 'params'), async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json({ data: user });
});
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
if (err instanceof ValidationError) {
return res.status(422).json({ error: 'Invalid request', fields: err.fieldErrors });
}
res.status(500).json({ error: 'Internal server error' });
};
This snippet shows how runtime request validation is layered onto an Express API using Zod, so that untrusted HTTP input is parsed into fully typed values before any handler runs. The core problem is that TypeScript types vanish at runtime: a route typed to receive { email: string } will happily receive undefined or a nested object from a malicious client. Zod closes that gap by describing the shape once and both validating at runtime and inferring the static type from the same source.
In validate middleware, the validate factory accepts a schema keyed by request location (body, query, params) and returns an Express middleware. It runs schema.safeParse rather than parse so a failed parse becomes a structured result.error instead of a thrown exception, which keeps control flow explicit. On success the parsed value is written back onto req, meaning downstream handlers see coerced, defaulted, stripped data rather than the raw payload. On failure it forwards a ValidationError carrying error.flatten().fieldErrors, deferring the actual HTTP response to a central error handler.
The user schemas tab defines the contracts. createUserSchema demonstrates several practical touches: z.string().email() for format checks, z.coerce.number() to turn query strings into numbers, .default() to fill optional fields, and .strict() so unexpected keys are rejected rather than silently ignored. The exported CreateUserInput type is produced with z.infer, so the handler's types stay in lockstep with the validator automatically — changing the schema changes the type, preventing drift.
users router wires it together. Each route lists its validate middleware before the handler, and because validation already ran, the handler treats req.body as trusted and typed. The errorHandler translates a ValidationError into a 422 with per-field messages while letting other errors fall through to a generic 500.
The main trade-off is a small per-request parsing cost and the discipline of keeping schemas as the single source of truth. In return the boundary is airtight: every field entering the system is checked exactly once, at the edge, and the rest of the code can rely on well-formed data. This pattern is the standard reach for any public JSON API where inputs cannot be trusted.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.