Skip to main content

Express Request Validation

Introduction

When building web applications with Express.js, one crucial aspect of request handling is validation. Request validation is the process of checking if the incoming data from clients meets your application's requirements before processing it further. Proper validation helps prevent bugs, security vulnerabilities, and ensures data integrity.

In this tutorial, we'll explore why validation is important, examine different validation techniques, and learn how to implement robust request validation in Express applications.

Why Validate Requests?

Before diving into implementation details, let's understand why request validation is essential:

  1. Security: Prevents injection attacks and other security vulnerabilities
  2. Data Integrity: Ensures your application receives properly formatted data
  3. Better Error Handling: Provides meaningful error messages to users
  4. Reduced Server-Side Errors: Prevents crashes due to unexpected input formats
  5. Improved User Experience: Gives clear feedback about invalid inputs

Basic Validation Approaches

Manual Validation

The simplest form of validation is writing your own validation logic directly in your route handlers:

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

// Manual validation
if (!username || username.length < 3) {
return res.status(400).json({ error: 'Username is required and must be at least 3 characters' });
}

if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email is required' });
}

if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}

// If validation passes, proceed with registration
// ...registration logic

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

While this approach works for simple cases, it doesn't scale well as your application grows. The validation logic can become repetitive and hard to maintain.

Using Express-Validator

A more powerful approach is using the popular express-validator library, which provides a set of Express middleware functions for validation and sanitization.

Installation

First, install the library:

bash
npm install express-validator

Basic Usage

Here's how to use express-validator for the same registration endpoint:

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

app.use(express.json());

app.post(
'/register',
// Define validation rules
[
body('username')
.notEmpty().withMessage('Username is required')
.isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),

body('email')
.notEmpty().withMessage('Email is required')
.isEmail().withMessage('Must provide a valid email'),

body('password')
.notEmpty().withMessage('Password is required')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
],
// Handle the request
(req, res) => {
// Check for validation errors
const errors = validationResult(req);

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

// Validation passed, proceed with registration
// ...registration logic

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

The response for invalid inputs would look like:

json
{
"errors": [
{
"msg": "Username must be at least 3 characters",
"param": "username",
"location": "body"
}
]
}

Advanced Validations

Let's explore more advanced validation scenarios:

Custom Validators

You can create custom validation logic for complex rules:

javascript
body('username')
.custom(value => {
if (value === 'admin') {
throw new Error('Username "admin" is reserved');
}
return true;
})

Checking for Duplicate Users

javascript
body('email').custom(async (email) => {
// This is a simplified example - in a real app, you'd check a database
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new Error('Email already in use');
}
return true;
})
javascript
body('passwordConfirmation').custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Password confirmation does not match password');
}
return true;
})

Request Parameter and Query Validation

Express-validator isn't limited to request bodies. You can also validate URL parameters and query strings:

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

// Validate URL parameters
app.get(
'/users/:id',
[
param('id').isInt().withMessage('User ID must be an integer')
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

// Fetch user with the specified ID
// ...

res.json({ user: /* user data */ });
}
);

// Validate query parameters
app.get(
'/products',
[
query('page').optional().isInt({ min: 1 }).withMessage('Page must be a positive integer'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100')
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const page = req.query.page || 1;
const limit = req.query.limit || 10;

// Fetch products with pagination
// ...

res.json({ products: /* product data */ });
}
);

Building Reusable Validation Middleware

As your application grows, you'll want to create reusable validation middleware. Here's a pattern to follow:

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

// Validation rules for user registration
const registerValidation = [
body('username')
.notEmpty().withMessage('Username is required')
.isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email')
.notEmpty().withMessage('Email is required')
.isEmail().withMessage('Must provide a valid email'),
body('password')
.notEmpty().withMessage('Password is required')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters'),
];

// Validation rules for user login
const loginValidation = [
body('email')
.notEmpty().withMessage('Email is required')
.isEmail().withMessage('Must provide a valid email'),
body('password')
.notEmpty().withMessage('Password is required'),
];

// Middleware to check for validation errors
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};

// Usage in routes
app.post('/register', registerValidation, validate, registerController);
app.post('/login', loginValidation, validate, loginController);

This approach keeps your validation logic separate from your controllers and makes it reusable across different routes.

Real-World Example: Product API

Let's build a more complete example of a product API with validation:

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

app.use(express.json());

// Middleware to validate request
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};

// Validation rules
const productValidation = [
body('name')
.notEmpty().withMessage('Product name is required')
.isLength({ min: 3, max: 100 }).withMessage('Product name must be between 3 and 100 characters'),
body('price')
.notEmpty().withMessage('Price is required')
.isFloat({ min: 0 }).withMessage('Price must be a positive number'),
body('category')
.notEmpty().withMessage('Category is required')
.isIn(['electronics', 'clothing', 'books', 'food']).withMessage('Invalid category'),
body('inStock')
.optional()
.isBoolean().withMessage('inStock must be a boolean value')
];

// Create a product
app.post('/products', productValidation, validate, (req, res) => {
// At this point, we know the data is valid
const product = req.body;

// Save product to database (simplified)
// const savedProduct = await ProductModel.create(product);

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

// Get product by ID
app.get(
'/products/:id',
[
param('id').isInt().withMessage('Product ID must be an integer')
],
validate,
(req, res) => {
const productId = req.params.id;
// Fetch product from database (simplified)
// const product = await ProductModel.findById(productId);

const product = { id: productId, name: 'Sample Product', price: 99.99 };

if (!product) {
return res.status(404).json({ message: 'Product not found' });
}

res.json({ product });
}
);

// Search products
app.get(
'/products',
[
query('minPrice').optional().isFloat({ min: 0 }).withMessage('Minimum price must be a positive number'),
query('maxPrice').optional().isFloat({ min: 0 }).withMessage('Maximum price must be a positive number'),
query('category').optional().isIn(['electronics', 'clothing', 'books', 'food']).withMessage('Invalid category')
],
validate,
(req, res) => {
const { minPrice, maxPrice, category } = req.query;

// In a real app, you would query your database with these filters
// const products = await ProductModel.find({
// price: { $gte: minPrice || 0, $lte: maxPrice || Infinity },
// ...(category ? { category } : {})
// });

// Sample response
res.json({
filters: { minPrice, maxPrice, category },
products: [
{ id: 1, name: 'Laptop', price: 999.99, category: 'electronics' },
{ id: 2, name: 'T-shirt', price: 19.99, category: 'clothing' }
]
});
}
);

const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Best Practices for Request Validation

To implement robust validation in your Express applications, follow these best practices:

  1. Validate Early: Validate input data as early as possible in the request lifecycle.
  2. Be Specific: Provide specific error messages to help users correct their input.
  3. Sanitize Input: Always sanitize input data to prevent security vulnerabilities.
  4. Use Middleware: Implement validation as middleware to keep routes clean and maintainable.
  5. Consider Async Validation: For database-dependent validations (like checking if a username exists).
  6. Test Edge Cases: Make sure your validation handles edge cases like empty strings, null values, etc.
  7. Don't Trust the Client: Always validate on the server, even if you have client-side validation.

Combining with Other Security Measures

Request validation is just one layer of security. Consider combining it with:

  • Input Sanitization: To prevent XSS attacks
  • Rate Limiting: To prevent brute force attacks
  • CORS Configuration: To control which domains can access your API
  • Helmet.js: To set HTTP headers for security

Summary

In this guide, we've covered:

  • Why request validation is crucial for Express applications
  • How to perform basic manual validation
  • Using express-validator for more robust validation
  • Creating reusable validation middleware
  • Best practices for implementing validation

Proper request validation is a fundamental aspect of building secure and reliable web applications. By validating incoming data, you ensure that your application processes only well-formed data, which leads to fewer bugs and better security.

Additional Resources

Exercises

  1. Create an API endpoint for user registration with proper validation for username, email, password, and age.
  2. Add validation to an API that accepts file uploads, ensuring only certain file types and sizes are permitted.
  3. Implement validation for a search API that includes pagination, sorting, and filtering parameters.
  4. Build a complete CRUD API for a "Task" resource with appropriate validations for each endpoint.


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