← All Articles 📝 Code Patterns High By Nishant Kumar Verma Updated

Zod vs No Validation: What AI Code Skips and Why It Matters

AI-generated API routes trust all input by default. Learn why Zod schema validation is essential and how to retrofit it into existing endpoints.

SEC-010 SEC-002

Quick Answer

AI-generated API routes almost never validate input. They destructure req.body directly and pass untrusted data to database queries, file operations, and business logic. Zod (or similar runtime validation libraries) lets you define exact input schemas that reject malformed, oversized, or malicious payloads before they reach your application code. Without it, every endpoint is an injection and crash vector.

The Problem: AI Trusts All Input

When you prompt an AI to "create an API route for user registration," the generated code destructures req.body immediately and uses the values directly. There is no type checking, no length validation, no format verification, and no sanitization. This is not a minor oversight - it opens the door to mass assignment, oversized payloads and injection.

The OWASP API Security Top 10 (2023) ranks Broken Object Property Level Authorization third (it includes mass assignment, like the role field below) and Unrestricted Resource Consumption fourth. A schema that allows only known fields and caps their size is a direct defense against both.

AI-Generated Route vs Validated Route

// ❌ BAD - AI-generated API route (no validation)
app.post('/api/users', async (req, res) => {
  const { name, email, age, role } = req.body;
  // No validation - accepts anything
  // name could be 10MB of text
  // email could be "not-an-email"
  // age could be -1 or "DROP TABLE users"
  // role could be "admin" (privilege escalation!)

  const user = await db.insert(users).values({ name, email, age, role });
  res.json(user);
});
// ✅ GOOD - Zod-validated API route
import { z } from 'zod';

const createUserSchema = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().email().max(255).toLowerCase(),
  age: z.number().int().min(13).max(150),
  // role is NOT accepted from input - set server-side
});

app.post('/api/users', async (req, res) => {
  const result = createUserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.flatten(),
    });
  }

  const { name, email, age } = result.data;
  // role is always 'user' - never from client input
  const user = await db.insert(users).values({
    name, email, age, role: 'user'
  });
  res.json(user);
});

What Happens Without Validation

Attack Vector Unvalidated Input Result
SQL injection name: "'; DROP TABLE users;--" Database destruction
Privilege escalation role: "admin" Unauthorized admin access
Denial of service name: "A".repeat(10_000_000) Server memory exhaustion / crash
Type confusion age: "not a number" Runtime errors, NaN in calculations
XSS via stored data name: "<script>alert(1)</script>" Script execution when rendered
Prototype pollution {"__proto__": {"admin": true}} Object prototype manipulation

Validation alone does not stop SQL injection or stored XSS: the schema above accepts both of those name values. Use parameterized queries or an ORM that binds values, and escape user content when you render it.

Essential Zod Patterns for API Routes

import { z } from 'zod';

// String with constraints (trim first, so "   " fails min(1))
const nameSchema = z.string().trim().min(1).max(100);

// Email with normalization
const emailSchema = z.string().email().max(255).toLowerCase();

// Enum to prevent arbitrary values
const roleSchema = z.enum(['user', 'editor']);

// Number with range
const ageSchema = z.number().int().min(0).max(150);

// UUID for IDs (prevents injection via ID fields)
const idSchema = z.string().uuid();

// Optional with default
const pageSchema = z.number().int().min(1).default(1);
const limitSchema = z.number().int().min(1).max(100).default(20);

// Reject unknown keys (prevents mass assignment)
const updateSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.string().email().optional(),
}).strict(); // Rejects any keys not in the schema

Common Validation Mistakes

Mistake Why It Is Wrong Correct Approach
Validating on the client only Client validation is bypassed with curl or Postman Always validate server-side; client validation is UX only
Using TypeScript types for runtime safety TS types are erased at runtime, provide zero protection Use Zod for runtime validation, infer TS types from schemas
Checking only required fields Extra fields pass through and pollute data Use .strict() to reject unknown keys
No max length on strings Attacker sends megabytes of text Always set .max() on strings

How to Add Validation to Your Project

  1. Install Zod: npm install zod. It has no dependencies, and Zod 4 also ships a smaller zod/mini build for frontends where bundle size matters.
  2. Create a schemas directory. Define one schema file per resource (users.schema.ts, posts.schema.ts).
  3. Add middleware. Create a validate(schema) middleware that calls schema.safeParse(req.body) and returns 400 on failure.
  4. Use .strict() on all schemas to reject unexpected fields and prevent mass assignment.
  5. Infer TypeScript types from Zod schemas: type CreateUser = z.infer<typeof createUserSchema>. One source of truth for both runtime and compile-time types.
  6. Scan for missing validation. VibeDoctor's Vibe Check detects API routes that accept request body data without validation and flags them as medium-severity findings.

Validation is one layer. If your routes also build SQL through Prisma, check them against our guide to Prisma raw query injection.

FAQ

Why Zod instead of Joi, Yup, or class-validator?

Zod is TypeScript-first with zero external dependencies. It provides first-class TypeScript type inference from schemas, so you don't maintain types separately. Joi and Yup work too, but in Zod the TypeScript types come straight from the schema by design. class-validator requires decorators and class-based patterns that don't fit functional API routes.

Does TypeScript not prevent these issues?

No. TypeScript types exist only at compile time and are completely erased when your code runs. Express types req.body as any, and nothing checks it at runtime against whatever TypeScript interface you declare. You need runtime validation (Zod) to actually check the data coming from the network. TypeScript tells you what shape data should be; Zod ensures it actually is.

Should I validate query parameters and URL params too?

Yes. Query parameters are strings by default and need parsing and validation. URL parameters (like :id) should be validated as UUIDs or numbers, not passed raw to database queries. z.coerce.number() is useful for query params that come as strings but should be numbers.

What about file uploads?

Zod validates JSON payloads. For file uploads, validate file type (MIME type and magic bytes, not just extension), file size, and filename separately. Use multer or busboy with explicit limits. Never trust the client-provided filename or content type without server-side verification.

How much does validation slow down my API?

Negligible. Zod validates a typical request body in under 0.1ms. The database query that follows takes 1-50ms. Validation overhead is less than 1% of total request time. The security benefit vastly outweighs the microsecond cost.

Diagnose your codebase - free

VibeDoctor checks for SEC-010, SEC-002 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →