Skip to main content

Express Validation Errors

When building web applications with Express.js, ensuring that user input meets your application's requirements is a critical part of maintaining data integrity and security. Form validation helps verify that users provide the correct information in the expected format. In this guide, we'll explore how to handle validation errors in Express applications in a way that's both user-friendly and maintainable.

Understanding Validation Errors

Validation errors occur when the data submitted by users doesn't meet the requirements defined by your application. These could be:

  • Required fields left empty
  • Email addresses in an invalid format
  • Passwords that don't meet security requirements
  • Numbers outside of acceptable ranges
  • Strings that are too short or too long

Unlike server errors (500s) or client errors like "not found" (404), validation errors typically don't indicate that something is broken—they're an expected part of the application flow.

Setting Up Validation in Express

To handle validation in Express, we typically use middleware. One popular validation library is express-validator. Let's start by installing it:

bash
npm install express-validator

Basic Validation Example

Here's how you might set up 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',
// Define validation rules
[
body('username').isLength({ min: 5 }).withMessage('Username must be at least 5 characters'),
body('email').isEmail().withMessage('Please provide a valid email'),
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
],
(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.status(201).json({ message: 'User registered successfully' });
}
);

Understanding the Response

When validation fails, the above code returns a response like this:

json
{
"errors": [
{
"value": "user",
"msg": "Username must be at least 5 characters",
"param": "username",
"location": "body"
},
{
"value": "invalid-email",
"msg": "Please provide a valid email",
"param": "email",
"location": "body"
}
]
}

This provides clear feedback about what went wrong and where, which is essential for good user experience.

Creating a Reusable Validation Middleware

To avoid repeating the error handling code, let's create a reusable validation middleware:

javascript
function validateRequest(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}

Now we can use this in our routes:

javascript
app.post(
'/register',
[
body('username').isLength({ min: 5 }).withMessage('Username must be at least 5 characters'),
body('email').isEmail().withMessage('Please provide a valid email'),
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
],
validateRequest,
(req, res) => {
// All validations passed
res.status(201).json({ message: 'User registered successfully' });
}
);

Advanced Validation Techniques

Custom Validators

You can create custom validators for complex validation rules:

javascript
body('password').custom((value, { req }) => {
if (value === req.body.username) {
throw new Error('Password cannot be the same as username');
}
return true;
})

Cross-Field Validation

Sometimes you need to validate fields against each other:

javascript
body('confirmPassword').custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Passwords do not match');
}
return true;
})

Sanitization

Validation often goes hand-in-hand with sanitization (cleaning user input):

javascript
body('email').isEmail().normalizeEmail(),
body('username').trim().escape()

Displaying Errors to Users

API Responses

For RESTful APIs, returning a structured JSON object makes it easy for client applications to display errors:

javascript
app.post('/login', [
body('email').isEmail().withMessage('Please enter a valid email'),
body('password').exists().withMessage('Password is required')
], (req, res) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map(err => ({
field: err.param,
message: err.msg
}))
});
}

// Continue with login process
});

Web Forms with Server-Side Rendering

If you're using server-side rendering with a template engine like EJS or Pug, you might handle errors like this:

javascript
app.post('/login', [
body('email').isEmail().withMessage('Please enter a valid email'),
body('password').exists().withMessage('Password is required')
], (req, res) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
// Convert errors to an object keyed by field name
const errorMap = errors.array().reduce((map, err) => {
map[err.param] = err.msg;
return map;
}, {});

// Render the login page again with errors
return res.render('login', {
errors: errorMap,
formData: req.body // To preserve form values
});
}

// Continue with login process
});

In your template file (e.g., login.ejs):

html
<form method="post" action="/login">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="<%= locals.formData?.email || '' %>">
<% if (locals.errors?.email) { %>
<span class="error"><%= errors.email %></span>
<% } %>
</div>

<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password">
<% if (locals.errors?.password) { %>
<span class="error"><%= errors.password %></span>
<% } %>
</div>

<button type="submit">Login</button>
</form>

Real-World Example: User Profile Update

Let's implement a comprehensive example for updating a user profile:

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

app.use(express.json());

// Validation middleware
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};

// User profile update endpoint
app.put(
'/users/:userId/profile',
[
// Validate URL parameter
param('userId').isMongoId().withMessage('Invalid user ID format'),

// Validate request body fields
body('name')
.optional()
.isLength({ min: 2, max: 50 })
.withMessage('Name must be between 2 and 50 characters'),

body('email')
.optional()
.isEmail()
.withMessage('Please provide a valid email')
.normalizeEmail(),

body('age')
.optional()
.isInt({ min: 13, max: 120 })
.withMessage('Age must be between 13 and 120'),

body('website')
.optional()
.isURL()
.withMessage('Website must be a valid URL'),

// Custom validation: prevent certain usernames
body('username')
.optional()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters')
.custom(value => {
const reserved = ['admin', 'root', 'system'];
if (reserved.includes(value.toLowerCase())) {
throw new Error('This username is reserved');
}
return true;
})
],
validate,
(req, res) => {
// If we get here, all validations passed
// In a real app, you would update the user profile in your database

res.json({
success: true,
message: 'Profile updated successfully',
data: {
userId: req.params.userId,
...req.body
}
});
}
);

Best Practices for Validation Errors

  1. Be specific: Error messages should clearly explain what's wrong and how to fix it.

  2. Group related validations: For complex forms, organize validations into logical groups.

  3. Validate on both client and server: Client-side validation improves user experience, but server-side validation is essential for security.

  4. Don't expose sensitive information: Error messages shouldn't reveal implementation details.

  5. Use consistent formats: Your error responses should follow a consistent structure throughout the application.

  6. Consider internationalization: For applications supporting multiple languages, ensure error messages can be translated.

Summary

Handling validation errors effectively is an important part of building robust Express applications. We've learned:

  • How to set up basic validation with express-validator
  • How to create reusable validation middleware
  • Advanced validation techniques like custom validators and cross-field validation
  • Displaying errors in both API responses and server-rendered applications
  • Best practices for validation error handling

By implementing proper validation error handling, you create a better user experience and protect your application from invalid data.

Further Resources

Exercises

  1. Create a login form validation that checks for a valid email and ensures the password is at least 8 characters with at least one uppercase letter, one lowercase letter, and one number.

  2. Implement a product creation API endpoint that validates product details: name (required, 3-100 chars), price (required, positive number), category (must be one of predefined list), and description (optional, max 1000 chars).

  3. Add custom validation for a user registration form that checks if the email is already registered in the database (this requires async validation).

  4. Create a validation middleware for file uploads that checks file types, sizes, and dimensions for image uploads.



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