Express API Validation
When building REST APIs with Express, validating incoming data is a critical step that ensures your application receives the expected data format before processing it. In this guide, we'll explore how to implement robust validation for your Express APIs.
Why Validate API Input?
Before diving into implementation, let's understand why validation is essential:
- Security: Prevents injection attacks and malicious data
- Data Integrity: Ensures your database only stores valid data
- Better Error Messages: Provides meaningful feedback to API consumers
- Reduced Server Errors: Catches problems early in the request lifecycle
Getting Started with Express Validation
Express doesn't come with built-in validation, but several excellent packages make it straightforward. We'll focus on express-validator
, one of the most popular validation libraries.
Installation
First, let's install the required packages:
npm install express express-validator
Basic Setup
Let's set up a simple Express application with validation:
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Basic Validation Examples
Let's create an endpoint to register a user with validation:
app.post(
'/api/users',
[
// Validate username
body('username')
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters')
.isAlphanumeric()
.withMessage('Username must contain only letters and numbers'),
// Validate email
body('email')
.isEmail()
.withMessage('Must provide a valid email address')
.normalizeEmail(),
// Validate password
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters')
],
(req, res) => {
// Handle validation results
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process valid data
const { username, email, password } = req.body;
// In a real app, you would hash the password and save to database
return res.status(201).json({
message: 'User registered successfully',
user: { username, email }
});
}
);
Example Request and Response
Valid request:
// POST /api/users
{
"username": "johndoe",
"email": "[email protected]",
"password": "secure123"
}
Response (201 Created):
{
"message": "User registered successfully",
"user": {
"username": "johndoe",
"email": "[email protected]"
}
}
Invalid request:
// POST /api/users
{
"username": "j$",
"email": "notanemail",
"password": "123"
}
Response (400 Bad Request):
{
"errors": [
{
"value": "j$",
"msg": "Username must be at least 3 characters",
"param": "username",
"location": "body"
},
{
"value": "j$",
"msg": "Username must contain only letters and numbers",
"param": "username",
"location": "body"
},
{
"value": "notanemail",
"msg": "Must provide a valid email address",
"param": "email",
"location": "body"
},
{
"value": "123",
"msg": "Password must be at least 6 characters",
"param": "password",
"location": "body"
}
]
}
Advanced Validation Techniques
Custom Validators
Sometimes the built-in validators aren't enough. Here's how to create custom validators:
app.post(
'/api/products',
[
body('name').notEmpty().withMessage('Product name is required'),
body('price')
.isFloat({ min: 0.01 })
.withMessage('Price must be a positive number'),
body('category')
.custom(value => {
const allowedCategories = ['electronics', 'books', 'clothing', 'food'];
if (!allowedCategories.includes(value)) {
throw new Error(`Category must be one of: ${allowedCategories.join(', ')}`);
}
return true;
})
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process the valid product data
return res.status(201).json({
message: 'Product created',
product: req.body
});
}
);
Validating Route Parameters
You can validate URL parameters as well:
const { param } = require('express-validator');
app.get(
'/api/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 ID
const userId = req.params.id;
// In a real app: const user = await User.findById(userId);
return res.json({
message: `Retrieved user ${userId}`
});
}
);
Validating Query Parameters
You can validate query string parameters too:
const { query } = require('express-validator');
app.get(
'/api/products/search',
[
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')
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { minPrice, maxPrice } = req.query;
// Process the search with validated parameters
return res.json({
message: 'Search successful',
filters: { minPrice, maxPrice },
results: [] // In a real app, this would contain actual search results
});
}
);
Creating Reusable Validation Schemas
As your API grows, you'll want to organize your validation logic. Here's a pattern to create reusable validation schemas:
// validations/user.js
const { body } = require('express-validator');
const userValidation = {
create: [
body('username')
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters')
.isAlphanumeric()
.withMessage('Username must contain only letters and numbers'),
body('email')
.isEmail()
.withMessage('Must provide a valid email address'),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters')
],
update: [
body('username')
.optional()
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters')
.isAlphanumeric()
.withMessage('Username must contain only letters and numbers'),
body('email')
.optional()
.isEmail()
.withMessage('Must provide a valid email address')
]
};
module.exports = userValidation;
Then in your routes file:
// routes/users.js
const express = require('express');
const router = express.Router();
const { validationResult } = require('express-validator');
const userValidation = require('../validations/user');
// Create user endpoint with validation
router.post('/', userValidation.create, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process the user creation
// ...
});
// Update user endpoint with validation
router.put('/:id', userValidation.update, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process the user update
// ...
});
module.exports = router;
Creating a Validation Middleware
To avoid repeating the validation result check, you can create a middleware:
// middlewares/validate.js
const { validationResult } = require('express-validator');
const validate = validations => {
return async (req, res, next) => {
await Promise.all(validations.map(validation => validation.run(req)));
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
return res.status(400).json({ errors: errors.array() });
};
};
module.exports = validate;
Usage in routes:
const validate = require('../middlewares/validate');
const { body } = require('express-validator');
// Define the validation chain
const createUserValidation = [
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').isEmail().withMessage('Must provide a valid email address'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
];
// Apply it to the route
app.post('/api/users', validate(createUserValidation), (req, res) => {
// No need to check for errors here, they're handled by the middleware
// Process the user creation
res.status(201).json({ message: 'User created successfully', user: req.body });
});
Real-world Example: A Complete Blog API with Validation
Below is a more comprehensive example showing validation in action for a blog API:
const express = require('express');
const { body, param, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
// Validation middleware
const validate = validations => {
return async (req, res, next) => {
await Promise.all(validations.map(validation => validation.run(req)));
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
return res.status(400).json({ errors: errors.array() });
};
};
// Create post validation
const createPostValidation = [
body('title')
.trim()
.isLength({ min: 5, max: 100 })
.withMessage('Title must be between 5 and 100 characters'),
body('content')
.trim()
.isLength({ min: 10 })
.withMessage('Content must be at least 10 characters'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array')
.custom(tags => {
if (!tags.every(tag => typeof tag === 'string')) {
throw new Error('All tags must be strings');
}
return true;
}),
body('published')
.optional()
.isBoolean()
.withMessage('Published must be a boolean value')
];
// Post ID validation
const postIdValidation = [
param('id')
.isInt({ min: 1 })
.withMessage('Post ID must be a positive integer')
];
// Create a new blog post
app.post('/api/posts', validate(createPostValidation), (req, res) => {
// Validation passed if we get here
const { title, content, tags = [], published = false } = req.body;
// In a real app, you would save to a database
const newPost = {
id: Date.now(),
title,
content,
tags,
published,
createdAt: new Date()
};
res.status(201).json({
message: 'Blog post created successfully',
post: newPost
});
});
// Get a blog post by ID
app.get('/api/posts/:id', validate(postIdValidation), (req, res) => {
const postId = parseInt(req.params.id);
// In a real app, you would fetch from a database
// For this example, we'll just simulate a found/not found scenario
const postExists = postId % 2 === 0; // Just for demonstration
if (!postExists) {
return res.status(404).json({ message: 'Post not found' });
}
const post = {
id: postId,
title: 'Sample Post',
content: 'This is a sample post content',
tags: ['sample', 'example'],
published: true,
createdAt: new Date()
};
res.json({ post });
});
// Update a blog post
app.put(
'/api/posts/:id',
validate([
...postIdValidation,
body('title')
.optional()
.trim()
.isLength({ min: 5, max: 100 })
.withMessage('Title must be between 5 and 100 characters'),
body('content')
.optional()
.trim()
.isLength({ min: 10 })
.withMessage('Content must be at least 10 characters'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array')
.custom(tags => {
if (!tags.every(tag => typeof tag === 'string')) {
throw new Error('All tags must be strings');
}
return true;
}),
body('published')
.optional()
.isBoolean()
.withMessage('Published must be a boolean value')
]),
(req, res) => {
const postId = parseInt(req.params.id);
const updates = req.body;
// In a real app, you would update the database record
res.json({
message: 'Blog post updated successfully',
post: {
id: postId,
...updates,
updatedAt: new Date()
}
});
}
);
// Start the server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Summary
API validation is a crucial aspect of building robust and secure Express applications. We've covered:
- Basic validation with
express-validator
- Validating different request parts (body, params, query)
- Creating custom validators
- Organizing validation logic into reusable schemas
- Building a validation middleware to DRY up your code
- Implementing comprehensive validation in a real-world example
By implementing thorough validation, you ensure your APIs are more secure, provide better user feedback, and maintain data integrity throughout your application.
Additional Resources
- Express-validator Documentation
- Joi - An alternative validation library
- Yup - Another schema validation library
Exercises
- Create an API endpoint for a product inventory that validates product details (name, price, quantity, category)
- Implement validation for a user profile update endpoint that ensures phone numbers are valid
- Build an API for a forum application that validates post creation with appropriate length limits for titles and content
- Create a validation middleware that includes both synchronous and custom asynchronous validators (e.g., checking if an email already exists in the database)
Remember that validation is your first line of defense against bad data - invest time in making it robust!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)