Validating incoming API requests before processing prevents a wide range of bugs, security issues, and confusing downstream errors. This guide covers implementing thorough, maintainable request validation.
Why Validate at the API Boundary
Every request entering your API is untrusted input, regardless of how "trusted" the client seems — validating at the boundary (before business logic runs) catches malformed data early, produces clearer error messages, and prevents invalid data from propagating deeper into your system.
Using a Schema Validation Library (Recommended Over Manual Checks)
npm install zod
const { z } = require('zod');
const CreateProductSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'food']),
});
app.post('/api/products', (req, res) => {
const result = CreateProductSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
const validatedData = result.data;
});
A schema library provides declarative, maintainable validation, automatically generating clear error messages — far more sustainable than scattered manual if checks throughout your route handlers.
Validating Query Parameters and Path Parameters Too
const ProductQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(20),
category: z.string().optional(),
});
Don't validate only request bodies — query parameters, path parameters, and headers are equally untrusted input warranting the same validation discipline.
Returning Clear, Actionable Error Messages
{
"errors": [
{ "field": "price", "message": "Price must be a positive number" }
]
}
Vague errors ("Invalid input") frustrate API consumers trying to debug their integration — specific, field-level error messages significantly improve the developer experience for anyone integrating with your API.
Sanitizing, Not Just Validating
Beyond checking format/type validity, consider whether input needs sanitization (trimming whitespace, normalizing case, stripping potentially dangerous characters for specific contexts) before use — validation confirms structure; sanitization prepares the actual value for safe use.
Validating Nested and Complex Structures
const OrderSchema = z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
})).min(1),
});
Schema libraries handle nested object/array validation cleanly — important for genuinely complex API payloads where manual validation logic becomes unwieldy.
Consistent Validation Middleware Pattern
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json({ errors: result.error.issues });
req.validatedData = result.data;
next();
};
}
app.post('/api/products', validate(CreateProductSchema), handler);
A reusable middleware pattern keeps validation logic consistent and DRY across your routes, rather than repeating validation boilerplate in every handler.
Relationship to General OWASP Input Validation Guidance
See Understanding and Mitigating the OWASP Top 10 Vulnerabilities — thorough input validation is a foundational defense against injection and several other vulnerability categories; it's a security practice, not just a data-quality one.
Common Errors
Validation passes but a downstream error still occurs with "invalid" data — ensure your schema genuinely captures all the constraints your business logic actually requires, not just basic type checking; schema validation should reflect real business rules, not just superficial structure.
Continue Reading
- How to Build and Secure a REST API on a VPS
- Understanding and Mitigating the OWASP Top 10 Vulnerabilities
- How to Implement Idempotent API Endpoints
Browse more articles in Object Storage, Messaging & APIs.