CSE 340: Web Backend Development

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:

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:

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:

In addition, you will sanitize the input data as follows:

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.

  1. Import the validation functions from express-validator. Add the following to the top of the src/controllers/organizations.js file:
  2. import { body, validationResult } from 'express-validator';
    
  3. Define the validation rules for the organization form fields. Add the following to the src/controllers/organizations.js file, placing it after the import statements and before the route handlers:
  4. // 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')
    ];
    
  5. Export the validation rules so they can be used in your route handlers. Add an export statement for organizationValidation to the the list of other exports at the bottom of the src/controllers/organizations.js file. It should now look something like this:
  6. export {
        showOrganizationsPage,
        showOrganizationDetailsPage,
        showNewOrganizationForm,
        processNewOrganizationForm,
        organizationValidation
    };
    

Apply Validation to Form Submission Route

  1. Open your src/controllers/routes.js file where all of your routes are defined.
  2. Update the import statement to include organizationValidation from the organizations controller. It should now look something like the following:
  3. import {
        showOrganizationDetailsPage,
        showNewOrganizationForm,
        processNewOrganizationForm,
        organizationValidation
    } from './organizations.js';
    
  4. 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:
  5. // 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.

  1. Open your src/controllers/organizations.js file.
  2. Update the processNewOrganizationForm function to check for validation errors using validationResult(). 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:
  3. 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.

  1. Start your server if it is not already running.
  2. Open your web browser and navigate to the new organization form at http://localhost:3000/new-organization.
  3. 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").
  4. Verify that appropriate error messages are displayed with flash messages for each invalid input.
  5. 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.

  1. Open the new organization form template file located at src/views/new-organization.ejs.
  2. Add HTML5 validation attributes to the form fields to enforce client-side validation. Include the following attributes:
    • For the organization name input, add required and maxlength="150" attributes.
    • For the description textarea, add required and maxlength="500" attributes.
    • For the contact email input, change the type from "text" to type="email" and add a required attribute.
  3. 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.

  4. Save the changes to the template file.
  5. Refresh the new organization form in your web browser.
  6. 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.
  7. Verify that the server-side validation is still present, by testing an organization name with only 1 character.
  8. Finally, ensure that if you submit the form with valid inputs the organization can be created successfully.

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: