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 is the single most common vulnerability in web applications.
The OWASP API Security Top 10 (2023) lists Broken Object Level Authorization and Unrestricted Resource Consumption as the top two API risks, both of which are directly enabled by missing input validation. According to Snyk's 2024 State of Open Source Security report, injection vulnerabilities remain the number one cause of security incidents in Node.js applications. A 2024 HackerOne report found that 73% of API vulnerabilities exploited in bug bounty programs involved improper input handling.
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().min(1).max(100).trim(),
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 |
Essential Zod Patterns for API Routes
import { z } from 'zod';
// String with constraints
const nameSchema = z.string().min(1).max(100).trim();
// 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);
// Strip 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
- Install Zod:
npm install zod. Its bundle size is 13KB minified (no dependencies). - Create a schemas directory. Define one schema file per resource (users.schema.ts, posts.schema.ts).
- Add middleware. Create a
validate(schema)middleware that callsschema.safeParse(req.body)and returns 400 on failure. - Use
.strict()on all schemas to reject unexpected fields and prevent mass assignment. - Infer TypeScript types from Zod schemas:
type CreateUser = z.infer<typeof createUserSchema>. One source of truth for both runtime and compile-time types. - Scan for missing validation. VibeDoctor (vibedoctor.io) detects API routes that accept request body data without validation and flags them as medium-severity findings.
FAQ
Why Zod instead of Joi, Yup, or class-validator?
Zod is TypeScript-native with zero external dependencies and the smallest bundle size (13KB). It provides first-class TypeScript type inference from schemas, so you don't maintain types separately. Joi and Yup work but are larger and don't offer the same level of TS integration. 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. req.body is typed as any at runtime regardless of what 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.