Skip to main content

Express Input Validation

Introduction

When building web applications with Express, one of the most critical security practices is proper input validation. Every piece of data that comes from external sources—whether from forms, URL parameters, query strings, or JSON payloads—should be treated as potentially dangerous until proven otherwise.

Input validation is the process of verifying that user-supplied data meets certain criteria before processing it. Failing to validate input properly can lead to various security vulnerabilities, including:

  • SQL Injection attacks
  • Cross-Site Scripting (XSS) attacks
  • Command Injection
  • Denial of Service attacks
  • Data corruption

In this guide, we'll explore how to implement robust input validation in your Express applications to create more secure, reliable, and stable systems.

Why Input Validation Matters

Imagine building a user registration form that accepts an email address. Without validation, users could enter invalid email formats, empty strings, or even malicious scripts. Let's see a problematic example:

javascript
app.post('/register', (req, res) => {
// UNSAFE: No validation performed
const userEmail = req.body.email;

// Using unvalidated input directly
db.createUser({ email: userEmail });

res.send('User registered!');
});

This code accepts any input as an email without checking its validity or sanitizing it, creating potential security and data integrity issues.

Basic Validation Approaches

1. Manual Validation

The simplest approach is to write your own validation logic:

javascript
app.post('/register', (req, res) => {
const userEmail = req.body.email;

// Basic validation
if (!userEmail || !userEmail.includes('@') || userEmail.length > 100) {
return res.status(400).json({ error: 'Invalid email address' });
}

// Process valid input
db.createUser({ email: userEmail });
res.send('User registered!');
});

While simple, this approach becomes cumbersome and error-prone for complex validation requirements.

2. Regular Expressions

Regular expressions provide more sophisticated validation patterns:

javascript
app.post('/register', (req, res) => {
const userEmail = req.body.email;

// Email regex pattern
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;

if (!emailPattern.test(userEmail)) {
return res.status(400).json({ error: 'Invalid email format' });
}

// Process valid input
db.createUser({ email: userEmail });
res.send('User registered!');
});

Regular expressions are powerful but can be difficult to read and maintain.

Using Validation Libraries

For real-world applications, validation libraries offer robust, reusable solutions with built-in common validation patterns.

Express-Validator

Express-validator is one of the most popular validation libraries for Express applications.

First, install the library:

bash
npm install express-validator

Now, let's implement validation for a user registration endpoint:

javascript
const express = require('express');
const { body, validationResult } = require('express-validator');

const app = express();
app.use(express.json());

app.post(
'/register',
// Validation middleware
[
body('email')
.isEmail().withMessage('Enter a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters long')
.matches(/\d/).withMessage('Password must contain at least one number'),
body('name')
.notEmpty().withMessage('Name is required')
.trim()
.escape()
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

// At this point, inputs are valid and sanitized
const { email, password, name } = req.body;

// Process the registration
// db.createUser({ email, password, name })

res.status(201).json({ message: 'User registered successfully' });
}
);

This example demonstrates several important validation concepts:

  1. Field validation rules: Email format, password complexity, required fields
  2. Custom error messages that help users understand what went wrong
  3. Input sanitization (trimming whitespace, escaping HTML)
  4. Structured error responses that can be easily processed by frontends

Validation Middleware

You can create reusable validation middleware for common validation patterns:

javascript
// validation-middleware.js
const { body, param, query, validationResult } = require('express-validator');

// User validation rules
const userValidationRules = () => {
return [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').notEmpty().trim()
];
};

// Product validation rules
const productValidationRules = () => {
return [
body('name').notEmpty().trim(),
body('price').isNumeric().withMessage('Price must be a number')
];
};

// Validation result middleware
const validate = (req, res, next) => {
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}

return res.status(400).json({ errors: errors.array() });
};

module.exports = {
userValidationRules,
productValidationRules,
validate
};

Using the middleware in routes:

javascript
const express = require('express');
const { userValidationRules, validate } = require('./validation-middleware');

const app = express();
app.use(express.json());

app.post('/register', userValidationRules(), validate, (req, res) => {
// All inputs are valid here
res.status(201).json({ message: 'User registered successfully' });
});

Advanced Validation Techniques

1. Schema Validation with Joi

For more complex objects, schema validation libraries like Joi offer a flexible solution:

javascript
const express = require('express');
const Joi = require('joi');

const app = express();
app.use(express.json());

// Define validation schema
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
name: Joi.string().required(),
age: Joi.number().integer().min(18).max(100)
});

app.post('/register', (req, res) => {
// Validate request body against schema
const { error, value } = userSchema.validate(req.body);

if (error) {
return res.status(400).json({ error: error.details[0].message });
}

// req.body data is valid
const { email, password, name, age } = value;

// Process registration
res.status(201).json({ message: 'Registration successful' });
});

2. Validating Different Request Parts

Express-validator can validate different parts of the request:

javascript
const express = require('express');
const { param, query, validationResult } = require('express-validator');

const app = express();

app.get(
'/users/:id/posts',
[
// Validate URL parameter
param('id').isInt().withMessage('User ID must be an integer'),

// Validate query parameters
query('limit').optional().isInt({ min: 1, max: 100 }),
query('sort').optional().isIn(['asc', 'desc'])
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

// Parameters are valid
const userId = req.params.id;
const { limit = 10, sort = 'asc' } = req.query;

// Fetch and return user posts
res.json({ userId, posts: [] });
}
);

3. Custom Validators

For domain-specific validation logic, you can create custom validators:

javascript
const { body } = require('express-validator');

app.post(
'/products',
[
body('sku')
.custom(async (value) => {
// Check if SKU already exists in database
const existingSku = await Product.findBySku(value);
if (existingSku) {
throw new Error('SKU already in use');
}
return true;
}),
body('releaseDate')
.custom(value => {
// Ensure release date is not in the past
const date = new Date(value);
if (date < new Date()) {
throw new Error('Release date cannot be in the past');
}
return true;
})
],
(req, res) => {
// Handle validation errors and process request
}
);

Real-World Example: API Endpoint with Complete Validation

Let's build a comprehensive example of a product creation API endpoint with thorough validation:

javascript
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();

app.use(express.json());

app.post(
'/api/products',
[
// Basic field validation
body('name')
.notEmpty().withMessage('Product name is required')
.trim()
.isLength({ min: 3, max: 100 }).withMessage('Name must be between 3 and 100 characters'),

body('price')
.notEmpty().withMessage('Price is required')
.isNumeric().withMessage('Price must be a number')
.custom(value => {
if (parseFloat(value) <= 0) {
throw new Error('Price must be greater than zero');
}
return true;
}),

body('category')
.notEmpty().withMessage('Category is required')
.isIn(['electronics', 'clothing', 'food', 'books']).withMessage('Invalid product category'),

body('tags')
.optional()
.isArray().withMessage('Tags must be an array'),

body('tags.*')
.optional()
.isString().withMessage('Each tag must be a string')
.trim()
.notEmpty().withMessage('Tags cannot be empty strings'),

body('stock')
.optional()
.isInt({ min: 0 }).withMessage('Stock must be a non-negative integer'),

body('description')
.optional()
.trim()
.isLength({ max: 1000 }).withMessage('Description cannot exceed 1000 characters'),

// Complex validation: Ensure sales_end_date is after sales_start_date
body('sales_start_date')
.optional()
.isISO8601().withMessage('Invalid date format. Use ISO format (YYYY-MM-DD)'),

body('sales_end_date')
.optional()
.isISO8601().withMessage('Invalid date format. Use ISO format (YYYY-MM-DD)')
.custom((value, { req }) => {
if (!req.body.sales_start_date) return true;

const startDate = new Date(req.body.sales_start_date);
const endDate = new Date(value);

if (endDate <= startDate) {
throw new Error('End date must be after start date');
}
return true;
})
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
status: 'error',
message: 'Validation failed',
errors: errors.array()
});
}

// All inputs are valid and sanitized
const product = {
name: req.body.name,
price: parseFloat(req.body.price),
category: req.body.category,
tags: req.body.tags || [],
stock: req.body.stock || 0,
description: req.body.description || '',
sales_start_date: req.body.sales_start_date,
sales_end_date: req.body.sales_end_date
};

// Process the product creation
// In a real app, you would save to database here
// db.createProduct(product)

res.status(201).json({
status: 'success',
message: 'Product created successfully',
data: {
product_id: 'new_product_id',
...product
}
});
}
);

// Start the server
app.listen(3000, () => console.log('Server running on port 3000'));

This example demonstrates:

  1. Comprehensive field validation for required fields, types, and formats
  2. Conditional validation based on relationships between fields
  3. Optional fields with appropriate validation when present
  4. Array validation for the tags field
  5. Domain-specific validation rules (e.g., price must be positive)
  6. Structured error responses that are frontend-friendly

Security Considerations

When implementing input validation, keep these security principles in mind:

  1. Defense in Depth: Input validation is just one layer of security. Always combine it with other security measures.

  2. Positive Validation: Specify what is allowed rather than what is disallowed. Whitelist, don't blacklist.

  3. Server-Side Validation: Never rely solely on client-side validation. Always validate on the server.

  4. Context-Specific Validation: Different fields require different validation rules based on their usage.

  5. Sanitization vs. Validation: Validation rejects invalid inputs, while sanitization transforms inputs into safe formats. Use both.

Common Mistakes to Avoid

  1. Incomplete Validation: Only validating some fields or some request types.

  2. Inadequate Error Handling: Not providing meaningful validation error messages.

  3. Validation Bypass: Allowing validation to be skipped in some execution paths.

  4. Trusting Client-Side Validation: Client-side validation can be bypassed; always validate on the server.

  5. Not Considering Edge Cases: Forgetting to validate null values, empty strings, or unexpected data formats.

Summary

Input validation is a critical security practice in Express applications. By properly validating and sanitizing all incoming data, you can:

  • Prevent various security vulnerabilities
  • Improve data quality and consistency
  • Provide better user experience with helpful error messages
  • Reduce the risk of application crashes and unexpected behavior

We've covered manual validation techniques, regular expressions, and validation libraries like Express-validator and Joi. We've also explored practical examples including middleware approaches, custom validators, and comprehensive API endpoint validation.

Remember that validation is just one aspect of a comprehensive security strategy, but it's an essential one that should never be overlooked.

Additional Resources

Practice Exercises

  1. Create a validation middleware for a product review endpoint that validates the review text, rating (1-5 stars), and user ID.

  2. Implement validation for a user profile update endpoint that handles different field types (email, URL, date, age).

  3. Build a custom validator that checks if a username is already taken in the database.

  4. Create a comprehensive validation scheme for a complex form submission with nested data structures.

  5. Implement conditional validation where certain fields are required only when specific conditions are met.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)