Express Request Filtering
Introduction
When building web applications with Express.js, handling incoming requests properly is crucial for maintaining security and ensuring your application processes only valid data. Request filtering refers to the process of examining, validating, and potentially modifying incoming HTTP requests before they reach your route handlers.
In this guide, you'll learn how to implement different request filtering techniques in Express.js applications, from basic validation to advanced security measures, all designed to keep your application robust and secure.
Why Request Filtering Matters
Before diving into implementation details, let's understand why request filtering is important:
- Security: Prevents malicious data from reaching your application logic
- Data Integrity: Ensures that your application receives properly formatted data
- Error Prevention: Catches invalid requests early, preventing downstream errors
- Performance: Rejects unwanted requests before expending resources on processing them
Basic Request Filtering with Middleware
Express middleware functions are the foundation of request filtering. They intercept requests before they reach route handlers, making them perfect for filtering.
Creating a Simple Filter Middleware
Here's a basic middleware function that checks if a required query parameter exists:
// Middleware to check if 'userId' query parameter exists
function checkUserIdParam(req, res, next) {
if (!req.query.userId) {
return res.status(400).send('Missing required userId parameter');
}
next();
}
// Using the middleware in a route
app.get('/user-data', checkUserIdParam, (req, res) => {
// This code only runs if userId exists in query
res.send(`Processing data for user: ${req.query.userId}`);
});
When a request is made to /user-data
without a userId parameter, the user will receive a 400 Bad Request response. Only requests with a valid userId will proceed to the route handler.
Filtering by Request Properties
Express provides access to various request properties that you can use for filtering:
Filtering by HTTP Method
Sometimes you want certain middleware to run only for specific HTTP methods:
function onlyForPOST(req, res, next) {
if (req.method !== 'POST') {
return res.status(405).send('Method not allowed');
}
next();
}
// Apply to specific routes
app.use('/api/data', onlyForPOST);
Filtering by Request Path
You can filter requests based on their path patterns:
function adminAreaFilter(req, res, next) {
if (req.path.startsWith('/admin')) {
// Check admin authentication here
if (!req.session.isAdmin) {
return res.status(403).send('Access forbidden');
}
}
next();
}
app.use(adminAreaFilter);
Filtering by Content Type
For APIs that expect specific formats:
function jsonOnly(req, res, next) {
if (req.get('Content-Type') !== 'application/json' && req.method !== 'GET') {
return res.status(415).send('Only JSON requests are accepted');
}
next();
}
app.use('/api', jsonOnly);
Data Validation and Sanitization
Proper request filtering often involves validating and sanitizing the data contained within requests.
Using express-validator
The express-validator
library provides powerful tools for validating and sanitizing request data:
const { body, validationResult } = require('express-validator');
app.post(
'/register',
// Validation rules
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters long'),
body('username').trim().isLength({ min: 3 }).withMessage('Username required')
],
// Request handler with validation check
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process valid registration
res.send('Registration successful');
}
);
In this example:
- We define validation rules for email, password, and username fields
- The email is validated and normalized (converted to a standard format)
- The password is checked for minimum length
- The username is trimmed of whitespace and checked for minimum length
- If any validation fails, a 400 status with error details is returned
Custom Validation Logic
For more complex validations, you can create custom middleware:
function validateProduct(req, res, next) {
const product = req.body;
// Required fields
if (!product.name || !product.price) {
return res.status(400).send('Product name and price are required');
}
// Type validation
if (typeof product.price !== 'number' || product.price <= 0) {
return res.status(400).send('Product price must be a positive number');
}
// Size limits
if (product.name.length > 100) {
return res.status(400).send('Product name too long (max 100 characters)');
}
// If everything is valid, proceed
next();
}
app.post('/products', validateProduct, (req, res) => {
// Handle valid product submission
res.send('Product added successfully');
});
Security Filtering
Request filtering is critical for application security. Here are some essential security filters:
Rate Limiting
Using the express-rate-limit
package to prevent abuse:
const rateLimit = require('express-rate-limit');
// Create limiter middleware
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
// Apply rate limiting to API routes
app.use('/api/', apiLimiter);
This middleware will track requests by IP address and limit them to 100 requests per 15-minute window.
Request Size Limiting
To prevent denial-of-service attacks via large payloads:
const express = require('express');
const app = express();
// Limit JSON payloads to 100kb
app.use(express.json({ limit: '100kb' }));
// Limit URL-encoded payloads to 100kb
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
Cross-Origin Resource Sharing (CORS)
Control which domains can access your API:
const cors = require('cors');
// Allow specific origins
const corsOptions = {
origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
Real-World Example: Complete API Filtering
Let's put everything together in a realistic API endpoint with comprehensive request filtering:
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(express.json());
// Rate limiting middleware
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 accounts per hour per IP
message: 'Too many accounts created from this IP, please try again after an hour'
});
// User registration endpoint with comprehensive filtering
app.post(
'/api/users',
createAccountLimiter, // Apply rate limiting
[
// Validation rules
body('email')
.isEmail().withMessage('Must provide a valid email')
.normalizeEmail()
.custom(async (email) => {
// Example check if email exists (in a real app, you'd check your DB)
if (email === '[email protected]') {
throw new Error('Email already in use');
}
}),
body('password')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
.matches(/\d/).withMessage('Password must contain a number')
.matches(/[A-Z]/).withMessage('Password must contain an uppercase letter'),
body('name')
.trim()
.isLength({ min: 2 }).withMessage('Name is required')
.matches(/^[a-zA-Z\s]+$/).withMessage('Name must contain only letters')
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// If we get here, all validations passed
const { email, password, name } = req.body;
// In a real app, you would create the user in your database
res.status(201).json({
message: 'User registered successfully',
user: { email, name }
});
}
);
app.listen(3000, () => console.log('Server running on port 3000'));
In this comprehensive example:
- We apply rate limiting to prevent abuse
- We validate the email format and check for uniqueness
- We enforce password complexity requirements
- We validate and sanitize the name field
- We return detailed validation errors when needed
- Only requests that pass all filters reach the registration logic
Advanced Request Filtering Patterns
Chaining Multiple Filters
You can chain multiple specialized filters for complex requirements:
// Authentication filter
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).send('Authentication required');
}
// Verify token and set user info on request object
req.user = verifyToken(token); // Implementation depends on your auth system
next();
}
// Permission filter
function requireAdmin(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).send('Admin privileges required');
}
next();
}
// Apply both filters to sensitive routes
app.delete('/api/users/:id', authenticate, requireAdmin, (req, res) => {
// Delete user logic
});
Dynamic Filtering Based on Configuration
You can create more flexible systems by using configuration-driven filters:
// Create a configurable permission filter
function requirePermission(permissionName) {
return (req, res, next) => {
if (!req.user || !req.user.permissions.includes(permissionName)) {
return res.status(403).send(`Required permission: ${permissionName}`);
}
next();
};
}
// Apply different permissions to different routes
app.get('/api/reports', authenticate, requirePermission('view_reports'), (req, res) => {
// Show reports
});
app.post('/api/products', authenticate, requirePermission('create_products'), (req, res) => {
// Create product
});
Summary
Request filtering is a critical aspect of building secure, robust Express applications. Through Express middleware, you can implement various filtering strategies to:
- Validate incoming data formats and values
- Enforce security policies and prevent attacks
- Limit request rates and sizes
- Control access based on authentication and permissions
- Normalize and sanitize data before processing
By applying these filtering techniques, you can protect your application from malicious or malformed requests while providing better error feedback to legitimate users.
Additional Resources
- Express.js Documentation - Official guide on using middleware
- express-validator Documentation - Comprehensive validation library
- OWASP Input Validation Cheat Sheet - Security best practices
- helmet - Security middleware collection for Express
Practice Exercises
-
Basic Filter: Create a middleware that validates a request has a valid API key in the headers.
-
Content Validation: Build a middleware that ensures POST requests to
/api/articles
have both a title (string, 5-100 chars) and content (string, 10-5000 chars). -
Advanced Security: Implement a filtering system that detects and blocks SQL injection attempts in query parameters.
-
Complete API: Create an Express route for a product search API that filters requests by validating search parameters and implements pagination limits.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)