Skip to content

09 Validation Joi Zod

DodaTech 4 min read

title: Validation with Joi and Zod in Node.js REST APIs weight: 19 date: 2026-06-28 lastmod: 2026-06-28 description: Learn request validation with Joi and Zod in Node.js REST APIs including schema definition, middleware integration, custom error messages, and validation testing. tags: [api-development, nodejs]


Validation with Joi and Zod defines schemas for request data in Node.js REST APIs, ensuring inputs meet requirements before reaching business logic and returning detailed error messages for invalid data.

```mermaid
flowchart TD
  A[Incoming Request] --> B[Validation Middleware]
  B --> C{Valid?}
  C -->|Yes| D[Route Handler]
  C -->|No| E[400 + Validation Details]
  D --> F[Business Logic]
  style A fill:#e1f5fe
  style B fill:#fff9c4
  style C fill:#c8e6c9
  style E fill:#ffcdd2

Joi and Zod are schema validation libraries. Joi is the traditional choice with a fluent API. Zod is newer with TypeScript-first design and type inference. Both define schemas for objects, strings, numbers, arrays, and custom validations, and return detailed error arrays on failure.

Think of validation like a bouncer at a club. The schema defines the rules: must be over 21 (minimum age), must have an ID (required field), no weapons (invalid patterns). The bouncer checks everyone and turns away those who do not meet the requirements.

Example: Joi Schema Validation

const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required()
    .messages({ 'string.min': 'Name must be at least 2 characters' }),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(150),
  role: Joi.string().valid('admin', 'editor', 'viewer').default('viewer'),
  tags: Joi.array().items(Joi.string()).max(5)
});

// Validation middleware
const validate = (schema) => {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true
    });

    if (error) {
      const details = error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message
      }));
      return res.status(400).json({
        status: 'error',
        error: { code: 'VALIDATION_ERROR', details }
      });
    }

    req.body = value;
    next();
  };
};

app.post('/api/users', validate(userSchema), userController.createUser);

Example: Zod Schema Validation

const { z } = require('zod');

const UserSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters').max(50),
  email: z.string().email('Invalid email format'),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'editor', 'viewer']).default('viewer'),
  tags: z.array(z.string()).max(5).optional()
});

// Type inference for TypeScript
type User = z.infer<typeof UserSchema>;

// Validation middleware
const validateZod = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);

  if (!result.success) {
    const details = result.error.issues.map(issue => ({
      field: issue.path.join('.'),
      message: issue.message
    }));
    return res.status(400).json({
      status: 'error',
      error: { code: 'VALIDATION_ERROR', details }
    });
  }

  req.body = result.data;
  next();
};

Example: Testing Validation

// Joi test
const { validateUser } = require('./validation');

test('validates correct user data', () => {
  const validUser = {
    name: 'Alice',
    email: 'alice@example.com',
    age: 30,
    role: 'admin'
  };
  const { error, value } = userSchema.validate(validUser);
  expect(error).toBeUndefined();
  expect(value.name).toBe('Alice');
});

test('rejects invalid email', () => {
  const invalidUser = {
    name: 'Bob',
    email: 'not-an-email'
  };
  const { error } = userSchema.validate(invalidUser);
  expect(error).toBeDefined();
  expect(error.details[0].path).toContain('email');
});

Expected output:

PASS  tests/validation.test.js
  ✓ validates correct user data (3 ms)
  ✓ rejects invalid email (1 ms)

Common Mistakes

  1. Validating only in the route handler — Validation should be middleware that runs before the handler. This keeps handlers clean and validation reusable.
  2. Not stripping unknown fields — Without stripUnknown, extra fields pass through and may cause security issues. Strip fields not in the schema.
  3. Aborting early on first error — abortEarly: false in Joi returns all validation errors, not just the first. This helps clients fix all issues at once.
  4. Throwing validation errors directly — Return validation errors as 400 responses, not 500. Use a validation middleware that returns formatted errors.
  5. Not validating all input sources — Validate req.body, req.params, req.query, and req.headers separately with appropriate schemas.

Practice Questions

  1. What is the difference between Joi and Zod?
  2. Why should validation be middleware instead of inline in route handlers?
  3. What does stripUnknown do in Joi?
  4. How do you validate query parameters with these libraries?
  5. Challenge: Create validation schemas for a user registration endpoint with Joi and Zod. Include name, email, password (min 8 chars, must have number and special char), and confirmPassword (must match password). Build middleware that validates body, params, and query.

FAQ

Should I use Joi or Zod?

Joi is older with more documentation. Zod has better TypeScript integration and type inference. Choose Zod if using TypeScript, Joi if you prefer the fluent API.

How do I validate nested objects?

Both libraries support nested schemas: Joi.object({ address: Joi.object({ city: Joi.string() }) }). Zod: z.object({ address: z.object({ city: z.string() }) }).

What is the best way to structure validation files?

Create one validation file per resource (userValidation.js, orderValidation.js). Export named schemas for create, update, and query operations.

Can I validate arrays of objects?

Yes: Joi.array().items(schema) or z.array(schema). Each item is validated against the schema.

How do I handle conditional validation?

Both support conditional rules: Joi.when() and Zod.refine() or z.discriminatedUnion(). For example, payment method required only if order total exceeds $100.

Mini Project

Build a validation middleware system for an Express API. Create schemas for user registration, order creation, and product filtering. The middleware should validate body, params, and query separately, return formatted error responses, and integrate with the error handling system.

What's Next

Now learn about authentication with JWT in Building REST APIs with Node.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro