Skip to main content

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:

bash
npm install express express-validator

Basic Setup

Let's set up a simple Express application with validation:

javascript
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:

javascript
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:

json
// POST /api/users
{
"username": "johndoe",
"email": "[email protected]",
"password": "secure123"
}

Response (201 Created):

json
{
"message": "User registered successfully",
"user": {
"username": "johndoe",
"email": "[email protected]"
}
}

Invalid request:

json
// POST /api/users
{
"username": "j$",
"email": "notanemail",
"password": "123"
}

Response (400 Bad Request):

json
{
"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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
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:

javascript
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

Exercises

  1. Create an API endpoint for a product inventory that validates product details (name, price, quantity, category)
  2. Implement validation for a user profile update endpoint that ensures phone numbers are valid
  3. Build an API for a forum application that validates post creation with appropriate length limits for titles and content
  4. 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! :)