DEV Community

Young Gao
Young Gao

Posted on

Zod vs Joi vs Class-Validator: Input Validation in TypeScript APIs Compared (2026)

Input Validation in TypeScript APIs: Zod vs Joi vs Class-Validator

Your API accepts a POST body. You trust it. A user sends { "age": "not a number" }. Your database query fails. Or worse, it succeeds with bad data.

Why Validate at the Boundary

TypeScript types disappear at runtime. req.body is any. Your internal functions trust typed parameters, but the data crossing your API boundary is untyped. Validate once at the edge, trust everywhere inside.

Zod (Recommended)

Schema-first. TypeScript-native. Infers types from schemas:

import { z } from "zod";
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).max(150),
  role: z.enum(["user", "admin"]).default("user"),
});

type CreateUser = z.infer<typeof CreateUserSchema>;
app.post("/users", (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (\!result.success) return res.status(400).json({ errors: result.error.flatten() });
  const user: CreateUser = result.data; // Fully typed\!
});
Enter fullscreen mode Exit fullscreen mode

Joi (Mature, Express-Friendly)

Joi has been around longer and has richer validation rules, but does not infer TypeScript types natively. You must maintain types separately.

Class-Validator (NestJS)

Decorator-based validation on class properties. Great with NestJS, heavyweight for plain Express.

Comparison

Feature Zod Joi Class-Validator
Type inference Yes No No
Bundle size Small Large Medium
Framework Any Express NestJS
Error messages Structured Detailed Basic

Validation Middleware Pattern

Create a reusable middleware factory:

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

app.post("/users", validate(CreateUserSchema), createUser);
app.put("/users/:id", validate(UpdateUserSchema), updateUser);
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

  1. Validating inside handlers: Creates code duplication. Use middleware.
  2. Not validating query params: Validate path params, query strings, and headers too, not just body.
  3. Exposing internal validation errors: Sanitize error messages. Do not leak field names or schema structure to attackers.
  4. Using .parse() instead of .safeParse(): .parse() throws. In Express, uncaught throws crash the process.

Part of my Production Backend Patterns series. Follow for more practical backend engineering.

Top comments (0)