W04 Learning Activity: Data Validation
Overview
When users submit forms on your website, you cannot trust that the data they provide is valid, complete, or safe. Form validation is the process of checking user input to ensure it meets your requirements before processing it. In this activity you will learn about both client-side validation (which improves user experience) and server-side validation (which protects your application).
Understanding validation is crucial because invalid data can break your application, corrupt your database, or even create security vulnerabilities. You will learn how validation has evolved from complex custom JavaScript to modern HTML5 features, and most importantly, how to properly validate data on the server side where it truly matters.
Preparation Material
The Evolution of Client-Side Validation
Before HTML5 introduced built-in validation features, developers had to write extensive JavaScript code to validate forms. This old approach required manually checking each field, displaying error messages, and preventing form submission when validation failed.
The Old Way: Custom JavaScript Validation
The following code shows the way developers used to validate a simple registration form with custom JavaScript. Notice how much code was needed for basic validation:
<form id="registrationForm">
<div>
<label for="email">Email:</label>
<input type="text" id="email" name="email">
<span id="emailError" class="error"></span>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<span id="passwordError" class="error"></span>
</div>
<div>
<label for="age">Age:</label>
<input type="text" id="age" name="age">
<span id="ageError" class="error"></span>
</div>
<button type="submit">Register</button>
</form>
The corresponding JavaScript validation code was lengthy and error-prone:
document.getElementById('registrationForm').addEventListener('submit', function(event) {
let isValid = true;
// Clear previous errors
document.getElementById('emailError').textContent = '';
document.getElementById('passwordError').textContent = '';
document.getElementById('ageError').textContent = '';
// Validate email
const email = document.getElementById('email').value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
document.getElementById('emailError').textContent = 'Email is required';
isValid = false;
} else if (!emailRegex.test(email)) {
document.getElementById('emailError').textContent = 'Please enter a valid email';
isValid = false;
}
// Validate password
const password = document.getElementById('password').value;
if (!password) {
document.getElementById('passwordError').textContent = 'Password is required';
isValid = false;
} else if (password.length < 8) {
document.getElementById('passwordError').textContent = 'Password must be at least 8 characters';
isValid = false;
}
// Validate age
const age = document.getElementById('age').value;
const ageNumber = parseInt(age);
if (!age) {
document.getElementById('ageError').textContent = 'Age is required';
isValid = false;
} else if (isNaN(ageNumber) || ageNumber < 13 || ageNumber > 120) {
document.getElementById('ageError').textContent = 'Please enter a valid age between 13 and 120';
isValid = false;
}
// Prevent submission if validation fails
if (!isValid) {
event.preventDefault();
}
});
This approach required dozens of lines of code for basic validation, and every form needed its own custom validation logic. Maintaining and debugging this code was time-consuming and error-prone.
The Modern Way: HTML5 Built-in Validation
HTML5 introduced powerful built-in validation features that accomplish the same goals with much less code. Modern browsers handle the validation logic, error display, and form submission prevention automatically.
<form>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required minlength="8">
</div>
<div>
<label for="age">Age:</label>
<input type="number" id="age" name="age" required min="13" max="120">
</div>
<button type="submit">Register</button>
</form>
This modern approach uses HTML5 input types and attributes to provide the same validation with zero JavaScript required. The browser automatically validates email format, enforces minimum password length, and ensures age is within the specified range.
Understanding Client-Side Validation Limitations
While client-side validation greatly improves user experience by providing immediate feedback, it is important to understand its limitations. Client-side validation is purely for convenience and user experience — it should never be considered a security measure.
Users can easily bypass client-side validation by disabling JavaScript, modifying HTML in browser developer tools, or sending requests directly to your server using tools like curl or Postman. Malicious users can submit any data they want regardless of your client-side validation rules.
<!-- A user could modify this in browser dev tools -->
<input type="email" required minlength="8">
<!-- And change it to this -->
<input type="text">
The primary benefits of client-side validation are improving user experience by catching errors immediately, reducing server load by preventing obviously invalid submissions, and providing helpful feedback before users waste time submitting incorrect data. However, you must always validate data on the server side where users cannot tamper with your validation logic.
Security Reminder
Client-side validation is for user experience only. Never rely on it for security, data integrity, or business logic enforcement. Always implement comprehensive server-side validation to protect your application.
Implementing Server-Side Validation
Server-side validation is where you enforce your actual business rules and security requirements. You must validate every piece of data that users submit, regardless of any client-side validation. This validation runs on your server where users cannot modify or bypass it.
Manual Validation Approach
You can implement basic validation using simple JavaScript logic. The following example shows the way validation could be added to a new user registration route:
app.post('/register', (req, res) => {
const { email, password, age } = req.body;
const errors = [];
// Validate email
if (!email) {
errors.push('Email is required');
} else if (!email.includes('@') || !email.includes('.')) {
errors.push('Please provide a valid email address');
}
// Validate password
if (!password) {
errors.push('Password is required');
} else if (password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
// Validate age
const ageNumber = parseInt(age);
if (!age) {
errors.push('Age is required');
} else if (isNaN(ageNumber) || ageNumber < 13 || ageNumber > 120) {
errors.push('Age must be between 13 and 120');
}
// Handle validation results
if (errors.length > 0) {
return res.status(400).json({
success: false,
errors: errors
});
}
// If validation passes, process the registration
console.log('Valid registration data:', { email, age: ageNumber });
res.json({
success: true,
message: 'Registration successful'
});
});
While this manual approach works, it becomes verbose and difficult to maintain as your validation requirements grow more complex. Professional applications typically use validation libraries to handle this logic more elegantly.
Using Express-Validator Library
The express-validator library provides a clean, powerful way to validate request data. It offers pre-built validators for common requirements and allows you to create custom validation rules. This library integrates seamlessly with Express and provides detailed error reporting.
After you install the library, you can import the validation functions into your project and start using them in your routes. The following code shows the necessary functions:
// Import validation functions using ES modules
import { body, validationResult } from 'express-validator';
Basic Validation Rules
Express-validator uses a middleware approach where you define validation rules as an array of middleware functions. Common validation methods include:
isEmail(): Validates that the input is a valid email format.isLength({ min, max }): Checks that the input length is within specified bounds.isInt({ min, max }): Validates that the input is an integer within a specified range.matches(regex): Validates that the input matches a given regular expression.notEmpty(): Ensures that the input is not empty.
The following example shows the way the new user registration route could be validated using express-validator:
// Define validation rules for registration
const registrationValidation = [
body('email')
.isEmail()
.withMessage('Please provide a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long'),
body('age')
.isInt({ min: 13, max: 120 })
.withMessage('Age must be between 13 and 120')
];
// Apply validation to the route
app.post('/register', registrationValidation, (req, res) => {
// Check for validation errors
const results = validationResult(req);
if (!results.isEmpty()) {
return res.status(400).json({
success: false,
errors: results.array()
});
}
// If validation passes, process the data
const { email, password, age } = req.body;
console.log('Valid registration:', { email, age });
res.json({
success: true,
message: 'Registration successful'
});
});
This approach separates validation rules from your route logic, making the code more readable and maintainable. The normalizeEmail() function automatically cleans up email formatting, and validationResult() collects all validation errors into a single object. As your application grows, you should move these validation rules to a separate file and import them where needed.
Advanced Validation Examples
Express-validator provides many built-in validators for common requirements. The following code shows examples of more sophisticated validation rules:
const advancedValidation = [
// Email validation with custom domain restrictions
body('email')
.isEmail()
.withMessage('Invalid email format')
.custom((value) => {
if (value.endsWith('@tempmail.com')) {
throw new Error('Temporary email addresses are not allowed');
}
return true;
})
.normalizeEmail(),
// URL validation
body('website')
.optional()
.isURL()
.withMessage('Please provide a valid website URL'),
// Phone number validation
body('phone')
.isMobilePhone()
.withMessage('Please provide a valid phone number'),
// Date validation
body('birthdate')
.isISO8601()
.withMessage('Please provide a valid date')
.custom((value) => {
const date = new Date(value);
const eighteenYearsAgo = new Date();
eighteenYearsAgo.setFullYear(eighteenYearsAgo.getFullYear() - 18);
if (date > eighteenYearsAgo) {
throw new Error('You must be at least 18 years old');
}
return true;
})
];
Creating Custom Validators
While express-validator provides many built-in validators, you often need to create custom validation logic for your specific business requirements. Custom validators allow you to implement complex rules that are unique to your application.
Password Complexity Validation
The following is an example of a custom validator that ensures passwords meet specific complexity requirements:
// Custom password validator
const passwordValidation = body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters long')
.custom((value) => {
// Check for at least one number
if (!/\d/.test(value)) {
throw new Error('Password must contain at least one number');
}
// Check for at least one special character
if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
throw new Error('Password must contain at least one special character');
}
// Check for at least one uppercase letter
if (!/[A-Z]/.test(value)) {
throw new Error('Password must contain at least one uppercase letter');
}
// Check for at least one lowercase letter
if (!/[a-z]/.test(value)) {
throw new Error('Password must contain at least one lowercase letter');
}
return true;
});
Username Uniqueness Validation
Custom validators can also perform asynchronous operations like database checks. The following is an example that ensures usernames are unique:
// Simulate a database check (you would use your actual database here)
const existingUsernames = ['admin', 'user', 'test', 'demo'];
const usernameValidation = body('username')
.isLength({ min: 3, max: 20 })
.withMessage('Username must be between 3 and 20 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores')
.custom(async (value) => {
// Simulate database lookup
if (existingUsernames.includes(value.toLowerCase())) {
throw new Error('Username is already taken');
}
return true;
});
Combining Multiple Custom Validators
You can combine multiple custom validators to create comprehensive validation suites for complex forms as shown in the following example:
const completeRegistrationValidation = [
// Email validation
body('email')
.isEmail()
.withMessage('Invalid email format')
.normalizeEmail(),
// Username validation
usernameValidation,
// Password validation
passwordValidation,
// Password confirmation
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('Passwords do not match');
}
return true;
}),
// Terms acceptance
body('acceptTerms')
.equals('true')
.withMessage('You must accept the terms and conditions')
];
// Use in route
app.post('/register', completeRegistrationValidation, (req, res) => {
const results = validationResult(req);
if (!results.isEmpty()) {
return res.status(400).json({
success: false,
errors: results.array()
});
}
// Process successful registration
const { email, username, password } = req.body;
console.log('New user registered:', { email, username });
res.json({
success: true,
message: 'Registration completed successfully'
});
});
Input Sanitization
Express-validator includes sanitization functions that clean and normalize user input. Sanitization helps prevent certain types of attacks and ensures data consistency.
Common sanitization methods include:
trim(): Removes leading and trailing whitespace.escape(): Escapes HTML characters to prevent XSS attacks.normalizeEmail(): Standardizes email formatting.toInt(): Converts input to an integer.customSanitizer(): Allows you to define your own sanitization logic.
The following example demonstrates how to use these sanitization methods in a validation chain:
const sanitizedValidation = [
body('name')
.trim() // Remove leading/trailing whitespace
.escape() // Escape HTML characters
.isLength({ min: 2, max: 50 })
.withMessage('Name must be between 2 and 50 characters'),
body('email')
.normalizeEmail() // Standardize email format
.isEmail()
.withMessage('Invalid email address'),
body('bio')
.trim()
.isLength({ max: 500 })
.withMessage('Bio cannot exceed 500 characters')
.customSanitizer((value) => {
// Remove potentially dangerous HTML tags
return value.replace(/<script>.*<\/script>/gi, '');
})
];
Never Trust User Input
Always validate and sanitize user input on the server side. Assume that any data coming from users could be malicious or incorrect. Implement validation as your first line of defense, but also use other security measures like rate limiting, authentication, and proper error handling.
Error Handling and User Feedback
When a validation error occurs, you should clearly communicate the issues to the user. Express-validator provides detailed error information that you can use to create helpful feedback messages.
Processing Validation Errors
The validationResult() function returns an object containing all validation errors. You can format these errors in various ways depending on your application's needs. The following code example shows the way these errors could be sent back for API responses (JSON) or for form rendering. Later in this activity, you will use Flash Messages to pass and display errors in your templates.
app.post('/register', registrationValidation, (req, res) => {
const results = validationResult(req);
if (!results.isEmpty()) {
// Get all errors as an array
const errorArray = results.array();
// Group errors by field
const errorsByField = results.mapped();
// Format for different response types
if (req.headers.accept === 'application/json') {
// API response format
return res.status(400).json({
success: false,
errors: errorArray,
fields: errorsByField
});
} else {
// Web form response - render page with errors
return res.render('register', {
title: 'Register',
errors: errorArray,
formData: req.body // Preserve user input
});
}
}
// Success handling
res.redirect('/dashboard');
});
Activity Instructions
In this activity, you will add validation checks to your new organization form. You will check the following:
- All three fields are required.
- Organization name is required and must be between 3 and 150 characters.
- Organization description is required and cannot exceed 500 characters.
- Organization email is required and must be a valid email format.
In addition, you will sanitize the input data as follows:
- Trim leading and trailing whitespace from all text fields.
- Escape HTML characters in the organization name and description to prevent XSS attacks.
- Normalize the email address to a standard format.
Install Express-Validator
Install the express-validator library, which provides professional-grade validation tools for Express applications. In your terminal, run the following command at the root directory of your project:
npm install express-validator
This library will handle server-side validation of form data, ensuring that only valid submissions are processed and stored in your database.
Add Validation Rules
Open your src/controllers/organizations.js file and add the following code for validation rules.
- Import the validation functions from express-validator. Add the following to the top of the
src/controllers/organizations.jsfile: -
Define the validation rules for the organization form fields. Add the following to the
src/controllers/organizations.jsfile, placing it after the import statements and before the route handlers: - Export the validation rules so they can be used in your route handlers. Add an export statement for
organizationValidationto the the list of other exports at the bottom of thesrc/controllers/organizations.jsfile. It should now look something like this:
import { body, validationResult } from 'express-validator';
// Define validation and sanitization rules for organization form
// Define validation rules for organization form
const organizationValidation = [
body('name')
.trim()
.notEmpty()
.withMessage('Organization name is required')
.isLength({ min: 3, max: 150 })
.withMessage('Organization name must be between 3 and 150 characters'),
body('description')
.trim()
.notEmpty()
.withMessage('Organization description is required')
.isLength({ max: 500 })
.withMessage('Organization description cannot exceed 500 characters'),
body('contactEmail')
.normalizeEmail()
.notEmpty()
.withMessage('Contact email is required')
.isEmail()
.withMessage('Please provide a valid email address')
];
export {
showOrganizationsPage,
showOrganizationDetailsPage,
showNewOrganizationForm,
processNewOrganizationForm,
organizationValidation
};
Apply Validation to Form Submission Route
- Open your
src/controllers/routes.jsfile where all of your routes are defined. - Update the import statement to include
organizationValidationfrom the organizations controller. It should now look something like the following: -
Add the validation rules to your form submission route. Locate the POST route that handles new organization submissions (
/new-organization) and modify it to include the validation middleware as follows:
import {
showOrganizationDetailsPage,
showNewOrganizationForm,
processNewOrganizationForm,
organizationValidation
} from './organizations.js';
// Route to handle new organization form submission
router.post('/new-organization', organizationValidation, processNewOrganizationForm);
Notice that when handling this post route, the server will now include the middleware function organizationValidation before sending it off to the controller function processNewOrganizationForm.
Handle Validation Results in the Controller
Now that the validation middleware is applied to the route, you can use the validation results in the controller function to respond appropriately to invalid input.
- Open your
src/controllers/organizations.jsfile. - Update the
processNewOrganizationFormfunction to check for validation errors usingvalidationResult(). If there are errors, add them to the flash messages and redirect the user back to the new organization form. The complete function should now look as follows:
const processNewOrganizationForm = async (req, res) => {
// Check for validation errors
const results = validationResult(req);
if (!results.isEmpty()) {
// Validation failed - loop through errors
results.array().forEach((error) => {
req.flash('error', error.msg);
});
// Redirect back to the new organization form
return res.redirect('/new-organization');
}
const { name, description, contactEmail } = req.body;
const logoFilename = 'placeholder-logo.png'; // Use the placeholder logo for all new organizations
const organizationId = await createOrganization(name, description, contactEmail, logoFilename);
req.flash('success', 'Organization added successfully!');
res.redirect(`/organization/${organizationId}`);
};
Notice that this code checks for validation errors after the form submission. If any errors are found, it redirects the user back to the new organization form and they will see a flash message with the validation error messages. If there are no errors it creates the new organization and redirects to the organization's detail page.
Test the Validation
Now that you have implemented validation for the new organization form, it's time to test it to ensure everything works as expected.
- Start your server if it is not already running.
- Open your web browser and navigate to the new organization form at
http://localhost:3000/new-organization. - Try submitting the form with various invalid inputs to trigger the validation rules you implemented. For example:
- Try leaving the fields empty.
- Try an organization name with just 1 character.
- Enter a description that exceeds 500 characters.
- Provide an invalid email format (e.g., "invalid-email").
- Verify that appropriate error messages are displayed with flash messages for each invalid input.
- Next, try submitting the form with valid inputs to ensure that the organization is created successfully and you are redirected to the organization's detail page.
Add client-side validation
Now that you have set up server-side validation, to make sure that your application only accepts proper data, it's important to also implement client-side validation. This provides immediate feedback to users and improves the overall user experience.
- Open the new organization form template file located at
src/views/new-organization.ejs. - Add HTML5 validation attributes to the form fields to enforce client-side validation. Include the following attributes:
- For the organization name input, add
requiredandmaxlength="150"attributes. - For the description textarea, add
requiredandmaxlength="500"attributes. - For the contact email input, change the type from
"text"totype="email"and add arequiredattribute.
- For the organization name input, add
- Save the changes to the template file.
- Refresh the new organization form in your web browser.
- Test the client-side validation by attempting to submit the form with invalid inputs. Verify that the browser prevents submission and displays appropriate error messages before the form is sent to the server.
- Verify that the server-side validation is still present, by testing an organization name with only 1 character.
- Finally, ensure that if you submit the form with valid inputs the organization can be created successfully.
Do not include minlength on the organization name.
While you could include a minlength attribute for client-side validation, in order to provide a case that you can test and demonstrate your server-side validation, do not include it here.
Next Step
Complete the other Week 04 Learning Activities
After you have completed all the learning activities for this lesson, return to Canvas to submit a quiz.
Other Links:
- Return to: Week Overview | Course Home