HVRDHVRD
JavaScript

Zod

Comprehensive guide explaining why input validation is essential, illustrated with detailed examples, and how to use Zod for schema-based validation in JavaScript applications.

Input Validation: Why It Matters

Input validation is the process of ensuring that data provided by users meets the expected format, type, and constraints before it is processed by your application.

Why Input Validation Is Crucial

  1. Prevent Security Vulnerabilities
    Invalid or malicious input can lead to serious security vulnerabilities such as:

    • SQL Injection
    • Cross-Site Scripting (XSS)
    • Command Injection
  2. Ensure Data Integrity
    Prevent storing invalid, malformed, or incomplete data in your database.

  3. Improve Application Stability
    Prevent runtime errors caused by invalid types or missing fields.

  4. Provide Clear User Feedback
    Allow users to know exactly what went wrong and how to fix their input.


Example: Why Input Validation Is Needed

Imagine a user registration endpoint without any validation:

app.post('/register', (req, res) => {
  const { username, email, age } = req.body;
  
  // Directly storing user input in database
  const user = new User({ username, email, age });
  
  user.save()
    .then(() => res.status(201).json({ message: 'User registered' }))
    .catch((err) => res.status(500).json({ error: err.message }));
});

Problem Scenario

A user sends this malicious payload:

{
  "username": "<script>alert('XSS')</script>",
  "email": "not-an-email",
  "age": "twenty"
}
  • username contains a script tag → Could trigger XSS attacks when displayed.
  • email is invalid.
  • age is a string, not a number.

Without validation, this data gets saved into the database or crashes the application.


Manual Input Validation Example

function validateUserInput(data) {
  if (typeof data.username !== 'string' || data.username.trim() === '') {
    throw new Error('Invalid username');
  }

  if (!data.email || !data.email.includes('@')) {
    throw new Error('Invalid email');
  }

  if (typeof data.age !== 'number' || data.age <= 0) {
    throw new Error('Invalid age');
  }
}

app.post('/register', (req, res) => {
  try {
    validateUserInput(req.body);

    const user = new User(req.body);
    user.save()
      .then(() => res.status(201).json({ message: 'User registered' }))
      .catch((err) => res.status(500).json({ error: err.message }));
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

Issues with Manual Validation

  • Tedious and error-prone
  • Repetitive for every endpoint
  • Hard to maintain and extend

Enter Zod: Type-Safe Schema Validation

Zod is a powerful TypeScript-first schema declaration and validation library for JavaScript and TypeScript.

Why Zod?

  • Declarative schema definitions
  • Type inference
  • Automatic error messages
  • Composable and reusable
  • Type-safe parsing of input
  • Easy integration with Express or any framework

Installing Zod

npm install zod

Detailed Zod Usage

Basic Schema Definition

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

const userSchema = z.object({
  username: z.string().min(3, 'Username must be at least 3 characters long'),
  email: z.string().email('Invalid email format'),
  age: z.number().int().positive('Age must be a positive integer'),
});

Parsing and Validation

try {
  const userData = userSchema.parse({
    username: 'harsha',
    email: 'harsha@example.com',
    age: 25,
  });

  console.log(userData);
  // { username: 'harsha', email: 'harsha@example.com', age: 25 }
} catch (err) {
  console.error(err.errors);
}

Using parse() either gives the data if valid or throws an error if invalid stopping execution. It is better to use safeParse() as it doesn't stop program execution and gives a more structured error messages.

Safe Parsing

Use safeParse() when you don’t want an exception thrown:

const result = userSchema.safeParse({
  username: 'ab',
  email: 'invalidemail',
  age: -5,
});

if (!result.success) {
  console.log(result.error.format());
}

This provides structured errors without throwing exceptions.


Middleware Integration Example

function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (result.success) {
      req.validatedBody = result.data;
      next();
    } else {
      res.status(400).json({ errors: result.error.format() });
    }
  };
}

app.post('/register', validate(userSchema), (req, res) => {
  const { username, email, age } = req.validatedBody;

  const user = new User({ username, email, age });
  user.save()
    .then(() => res.status(201).json({ message: 'User registered' }))
    .catch((err) => res.status(500).json({ error: err.message }));
});

Advanced Schema Features

Nested Schemas

const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zip: z.string().length(5),
});

const userWithAddressSchema = z.object({
  username: z.string(),
  email: z.string().email(),
  address: addressSchema,
});

Optional and Default Values

const schema = z.object({
  username: z.string(),
  age: z.number().optional(),
  isActive: z.boolean().default(true),
});

const result = schema.parse({ username: 'harsha' });
// result.isActive === true

Union Types

const schema = z.union([
  z.string(),
  z.number(),
]);

schema.parse(123);    // valid
schema.parse('abc');  // valid

Example Invalid Input Response

{
  "errors": {
    "username": { "_errors": ["Username must be at least 3 characters long"] },
    "email": { "_errors": ["Invalid email format"] },
    "age": { "_errors": ["Age must be a positive integer"] }
  }
}

Best Practices for Using Zod

  • Always define schemas close to expected inputs (request body, query params).
  • Use .default() and .optional() for optional fields.
  • Compose schemas for reusable pieces (e.g., address schema).
  • Prefer safeParse() over parse() in middleware to avoid exceptions.
  • Provide clear error messages for user-friendly API responses.