Color Mode

Building Your First Form: Contact Us

In this assignment, you will create a functional contact form that allows users to submit messages through your website. This form will demonstrate the complete cycle of web form processing: displaying a form to users, handling their submissions with POST requests, validating the data both on the client and server sides, and storing the results in your PostgreSQL database.

You will build this feature using proper MVC architecture, creating dedicated controllers for form handling, models for database operations, and views for user interaction. This assignment introduces server-side validation using the express-validator library, which provides professional-grade validation tools used in production web applications.

Understanding the Problem

Static websites can only display information, but dynamic web applications need to collect and process user input. Contact forms are one of the most common ways websites interact with their visitors, allowing users to ask questions, report issues, or provide feedback without requiring email clients or external services.

However, accepting user input creates several challenges that static sites never face. Users might submit empty forms, provide invalid data, or even attempt to inject malicious content. Your application needs to validate all incoming data, handle errors gracefully, and store valid submissions securely in your database.

This assignment addresses these challenges by implementing both client-side validation (for immediate user feedback) and server-side validation (for security and data integrity). You will create a database table for storing submissions, build routes and controllers to handle form processing, and implement comprehensive validation to ensure data quality.

Preparation

Before building the contact form, you need to create a database table to store contact form submissions. You will use a SQL file approach that follows the same pattern as your existing seed.sql file, but in a separate file dedicated to your practice assignments.

Create a new file at src/models/sql/practice.sql. This file will serve as a dedicated space for all your practice assignment database changes throughout the course. You will add the contact form table now, and future assignments may add additional tables or modifications to this same file. Add the following content:


        -- Practice database tables for assignments
        -- This file accumulates changes from multiple assignments
        -- Add new tables and modifications here as you work through the course

        -- Contact form table
        CREATE TABLE IF NOT EXISTS contact_form (
            id SERIAL PRIMARY KEY,
            subject VARCHAR(255) NOT NULL,
            message TEXT NOT NULL,
            submitted TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
    

Next, modify your src/models/setup.js file to execute this practice SQL file as part of the seeding process. Open setup.js and locate the line that says console.log('Database seeded successfully');. Immediately BEFORE this line (after the await db.query(seedSQL); but before the success log), add the following code:


        // Run practice.sql if it exists (for student assignments)
        const practicePath = join(__dirname, 'sql', 'practice.sql');
        if (fs.existsSync(practicePath)) {
            const practiceSQL = fs.readFileSync(practicePath, 'utf8');
            await db.query(practiceSQL);
            console.log('Practice database tables initialized');
        }
    

This code checks if practice.sql exists and executes it as part of the seed process. The file check ensures your application still works even if you have not created the practice file yet. This practice SQL will run every time you trigger a database re-seed by deleting data from the faculty table.

Understanding the SQL File Approach

You might wonder why we use a separate practice.sql file instead of putting everything in seed.sql. This separation keeps your practice assignments isolated from the core course database structure. The seed.sql file contains the baseline data that every student needs, while practice.sql contains your individual practice work that will grow over time.

Notice the use of CREATE TABLE IF NOT EXISTS in the SQL. This makes the script idempotent, meaning you can run it multiple times safely without errors. Every time the seed process runs (when the faculty table is empty), it will also run your practice SQL. The IF NOT EXISTS clause prevents errors if the table already exists.

Restart your server and verify that the table was created by checking your server console. You should see the message "Practice database tables initialized" after "Database seeded successfully". Use pgAdmin to examine your database tables and confirm that a new contact_form table exists with columns for id, subject, message, and submitted timestamp. Do not proceed until you confirm the table exists and your server starts without errors.

Keep in mind in order to re-seed the database and run the practice SQL again, you need to delete all data from the faculty table. This triggers the seed process, which now includes your practice SQL file.

Assignment Instructions

1. Install Express-Validator

Install the express-validator library, which provides professional-grade validation tools for Express applications:


        pnpm install express-validator
    
Why Use a Validation Library?

You might wonder why we use express-validator instead of writing our own validation functions. This connects back to an important principle from earlier in the course: be intentional about dependencies. While you could write custom validation code, using express-validator provides several important benefits that justify adding this dependency:

Security: express-validator is battle-tested by thousands of developers and handles security edge cases (like XSS prevention and proper input sanitization) that are easy to miss in custom code. Writing secure validation from scratch requires deep knowledge of attack vectors that most developers acquire only after years of experience.

Best Practices: It implements industry-standard validation patterns used in production applications, teaching you how professional developers handle validation. When you join a development team, you will almost certainly encounter this or a similar validation library.

Less Code, Fewer Bugs: Writing robust validation from scratch requires handling many edge cases. express-validator provides this functionality out of the box, reducing bugs and saving significant development time. The alternative would be hundreds of lines of custom validation code that would need thorough testing.

Feature-Rich: It includes built-in validators for common cases (email, URL, length, patterns) that would require significant custom code to replicate properly. These validators handle international characters, various formats, and edge cases that are easy to overlook.

In professional development, you will almost always use validation libraries rather than writing custom validation for every project. Learning express-validator now prepares you for real-world development practices and teaches you when adding a dependency is the right choice.

2. Configure Express to Handle POST Requests

Ensure your Express application is set up to parse URL-encoded form data from POST requests. Open your server.js file and add the following middleware in the section where you configure Express middleware, right after the code that serves static files:


        // Allow Express to receive and process POST data
        app.use(express.urlencoded({ extended: true }));
        app.use(express.json());
    

The first line enables parsing of form data sent with application/x-www-form-urlencoded content type, which is what HTML forms use by default. The second line enables parsing of JSON data, which will be useful for future assignments involving APIs.

This middleware configuration handles text-based form data and JSON, but it does not handle file uploads. File uploads require different middleware that processes multipart/form-data, which is covered in an optional assignment later in the course. For this contact form assignment, you will only work with text input.

3. Add Contact Form Styling

Create a CSS file to style your contact form. Create public/css/contact.css with the following content:


        /* Contact Form Styles */
        .contact-form {
            max-width: 600px;
            margin: 2rem auto;
            padding: 2rem;
            border: 1px solid #ddd;
            border-radius: 8px;
            background-color: #fff;

            h2 {
                color: #2c5aa0;
                border-bottom: 2px solid #2c5aa0;
                padding-bottom: 0.5rem;
                margin: 0 0 1.5rem 0;
                text-align: center;
            }

            .form-group {
                margin-bottom: 1.5rem;

                label {
                    display: block;
                    margin-bottom: 0.5rem;
                    font-weight: 500;
                    color: #333;
                }

                input,
                textarea {
                    width: 100%;
                    padding: 0.75rem;
                    border: 1px solid #ddd;
                    border-radius: 6px;
                    font-size: 1rem;
                    font-family: inherit;
                    transition: border-color 0.3s ease;

                    &:focus {
                        outline: none;
                        border-color: #2c5aa0;
                    }
                }

                textarea {
                    resize: vertical;
                    min-height: 150px;
                }
            }

            button[type="submit"] {
                width: 100%;
                padding: 1rem;
                background-color: #2c5aa0;
                color: white;
                border: none;
                border-radius: 6px;
                font-size: 1rem;
                font-weight: 500;
                cursor: pointer;
                transition: background-color 0.3s ease;

                &:hover {
                    background-color: #1e3a6f;
                }
            }
        }

        /* Contact Responses Styles */
        .contact-responses {
            max-width: 800px;
            margin: 2rem auto;

            .response-item {
                background-color: #fff;
                border: 1px solid #ddd;
                border-radius: 8px;
                padding: 1.5rem;
                margin-bottom: 1.5rem;

                .response-subject {
                    color: #2c5aa0;
                    margin: 0 0 1rem 0;
                    font-size: 1.25rem;
                }

                .response-message {
                    color: #333;
                    line-height: 1.6;
                    margin: 0 0 1rem 0;
                    white-space: pre-wrap;
                }

                .response-date {
                    color: #666;
                    font-size: 0.875rem;
                    margin: 0;
                }
            }

            .no-responses {
                text-align: center;
                color: #666;
                font-style: italic;
                padding: 2rem;
            }
        }
    

This CSS provides clean, modern styling for both the contact form and the responses page. The nested selectors use modern CSS nesting syntax to keep related styles organized together.

4. Set Up Dynamic CSS Loading

Add middleware to your routes configuration to load the contact form CSS only on contact-related pages. Open src/controllers/routes.js and add the following middleware with your other route-specific middleware (near the catalog and faculty middleware):


        // Add contact-specific styles to all contact routes
        router.use('/contact', (req, res, next) => {
            res.addStyle('<link rel="stylesheet" href="/css/contact.css">');
            next();
        });
    

This ensures the contact CSS file is loaded for any route starting with /contact, following the same pattern you implemented in the Housekeeping Refactor assignment.

5. Create the Contact Controller

Create the controller file for your contact form functionality. Create src/controllers/forms/contact.js. You might notice that this file will contain controller functions, validation rules, and route definitions all together in one file. This is a common and practical pattern in Express applications for features of this size.

Understanding Controller Organization

In larger frameworks like Ruby on Rails or ASP.NET MVC, controllers, routes, and validation are often separated into different files or layers. However, Express is a minimalist framework that gives you flexibility in how you organize code. For small to medium-sized features, combining related functionality into a single controller file is a standard and practical approach.

You might ask: "Isn't this breaking MVC separation?" Not at all. MVC separation means keeping your concerns organized: Models handle data and database operations, Views handle presentation, and Controllers handle request processing, validation, and coordination between models and views. Validation and routing are part of the controller layer, not separate concerns. What matters is that your database logic stays in models and your presentation logic stays in views.

This single-file approach keeps all contact form logic in one place, making it easier to understand and maintain. You can see the validation rules right next to the controller functions that use them, and the routes that connect everything together. As your application grows, you can split files if a controller becomes too large or if validation rules need to be reused across multiple controllers. For now, keeping related code together follows the principle of high cohesion and makes your codebase more maintainable.

Add the following content to src/controllers/forms/contact.js:


        import { Router } from 'express';
        import { body, validationResult } from 'express-validator';
        import { createContactForm, getAllContactForms } from '../../models/forms/contact.js';

        const router = Router();

        /**
         * Display the contact form page.
         */
        const showContactForm = (req, res) => {
            res.render('forms/contact/form', {
                title: 'Contact Us'
            });
        };

        /**
         * Handle contact form submission with validation.
         * If validation passes, save to database and redirect.
         * If validation fails, log errors and redirect back to form.
         */
        const handleContactSubmission = async (req, res) => {
            // Check for validation errors
            const errors = validationResult(req);
            
            if (!errors.isEmpty()) {
                // Log validation errors for developer debugging
                console.error('Validation errors:', errors.array());
                // Redirect back to form without saving
                return res.redirect('/contact');
            }

            // Extract validated data
            const { subject, message } = req.body;

            try {
                // Save to database
                await createContactForm(subject, message);
                console.log('Contact form submitted successfully');
                // Redirect to responses page on success
                res.redirect('/contact/responses');
            } catch (error) {
                console.error('Error saving contact form:', error);
                res.redirect('/contact');
            }
        };

        /**
         * Display all contact form submissions.
         */
        const showContactResponses = async (req, res) => {
            let contactForms = [];
            
            try {
                contactForms = await getAllContactForms();
            } catch (error) {
                console.error('Error retrieving contact forms:', error);
            }

            res.render('forms/contact/responses', {
                title: 'Contact Form Submissions',
                contactForms
            });
        };

        /**
         * GET /contact - Display the contact form
         */
        router.get('/', showContactForm);

        /**
         * POST /contact - Handle contact form submission with validation
         */
        router.post('/',
            [
                body('subject')
                    .trim()
                    .isLength({ min: 2 })
                    .withMessage('Subject must be at least 2 characters'),
                body('message')
                    .trim()
                    .isLength({ min: 10 })
                    .withMessage('Message must be at least 10 characters')
            ],
            handleContactSubmission
        );

        /**
         * GET /contact/responses - Display all contact form submissions
         */
        router.get('/responses', showContactResponses);

        export default router;
    

In this assignment, validation errors are logged to the console for developer debugging, and the user is simply redirected back to the form. In a future assignment, you will learn how to display helpful error messages to users when validation fails, providing better user experience. For now, the focus is on implementing secure server-side validation and understanding the validation workflow.

Now integrate these routes into your main routes file. Open src/controllers/routes.js, import the contact routes at the top with your other imports:


        import contactRoutes from './forms/contact.js';
    

Then add the contact routes to your router after your other route definitions:


        // Contact form routes
        router.use('/contact', contactRoutes);
    

6. Create the Contact Model

Create the model file that handles database operations for contact form submissions. Create src/models/forms/contact.js with the following content:


        import db from '../db.js';

        /**
         * Inserts a new contact form submission into the database.
         * 
         * @param {string} subject - The subject of the contact message
         * @param {string} message - The message content
         * @returns {Promise<Object>} The newly created contact form record
         */
        const createContactForm = async (subject, message) => {
            const query = `
                INSERT INTO contact_form (subject, message)
                VALUES ($1, $2)
                RETURNING *
            `;
            const result = await db.query(query, [subject, message]);
            return result.rows[0];
        };

        /**
         * Retrieves all contact form submissions, ordered by most recent first.
         * 
         * @returns {Promise<Array>} Array of contact form records
         */
        const getAllContactForms = async () => {
            const query = `
                SELECT id, subject, message, submitted
                FROM contact_form
                ORDER BY submitted DESC
            `;
            const result = await db.query(query);
            return result.rows;
        };

        export { createContactForm, getAllContactForms };
    
Parameterized Queries for Security

Notice how the model uses parameterized queries with $1 and $2 placeholders instead of concatenating user input directly into SQL strings. This is critical for preventing SQL injection attacks, where malicious users could manipulate your database by inserting SQL commands into form fields.

The PostgreSQL driver automatically escapes and sanitizes the values you pass in the second argument to db.query(), ensuring that user input is treated as data rather than executable SQL code. Never concatenate user input into SQL queries, even if you think the input is safe.

7. Understanding express-validator

The express-validator library provides middleware functions that validate incoming request data. In your controller file, you defined validation rules as an array of middleware functions in the route definition, before your controller function. Each validation rule runs in sequence, checking specific conditions about the submitted data.

The body() function creates a validator for a specific field in the request body. You can chain methods to specify validation rules:

In your controller function, the validationResult(req) function collects all validation errors from the request. If errors.isEmpty() returns false, validation failed and you should not process the data. The errors.array() method returns an array of error objects that you can log or display to users.

This separation of concerns keeps your validation rules close to your route definitions while keeping your controller functions focused on handling the validated data. The validation middleware runs before your controller function, ensuring that your controller only receives valid data.

Common Validation Methods

express-validator provides many built-in validation methods you can use:

  • isEmail() validates email addresses
  • isURL() validates URLs
  • isNumeric() ensures the value contains only numbers
  • isLength({ min, max }) checks string length
  • matches(pattern) validates against a regular expression
  • custom() creates custom validation logic

You can also chain sanitization methods like trim(), escape(), and normalizeEmail() to clean and standardize input data before validation.

8. Create the Contact Form Views

Create the user interface for your contact form. First, create the directory structure: src/views/forms/contact/. Then create src/views/forms/contact/form.ejs with the following content:


        <%- include('../../partials/header') %>
        <main>
            <h1>Contact Us</h1>
            <p>
                Have questions or feedback? Send us a message using the form below and we'll get back to you as soon as possible.
            </p>
            
            <form method="POST" action="/contact" class="contact-form">
                <div class="form-group">
                    <label for="subject">Subject</label>
                    <input type="text" id="subject" name="subject" placeholder="What is this message about?">
                </div>
                
                <div class="form-group">
                    <label for="message">Message</label>
                    <textarea id="message" name="message" rows="6" placeholder="Please provide details about your question or feedback..."></textarea>
                </div>
                
                <button type="submit">Send Message</button>
            </form>
        </main>
        <%- include('../../partials/footer') %>
    

Next, create src/views/forms/contact/responses.ejs to display submitted messages:


        <%- include('../../partials/header') %>
        <main>
            <h1>Contact Form Submissions</h1>
            
            <% if (contactForms && contactForms.length > 0) { %>
                <div class="contact-responses">
                    <% contactForms.forEach(form => { %>
                        <div class="response-item">
                            <h3 class="response-subject"><%= form.subject %></h3>
                            <p class="response-message"><%= form.message %></p>
                            <p class="response-date">Submitted: <%= new Date(form.submitted).toLocaleDateString() %></p>
                        </div>
                    <% }); %>
                </div>
            <% } else { %>
                <p class="no-responses">No contact form submissions yet.</p>
            <% } %>
        </main>
        <%- include('../../partials/footer') %>
    
Form Structure and Naming

Notice how the form uses the POST method to submit data to the server at the /contact endpoint. The input fields have name attributes that correspond to the keys expected in the controller's request body. This naming consistency is crucial for the server to correctly parse and process submitted data.

Since the contact form views sit two levels deep in the directory structure (forms/contact/), the relative paths to shared partials require going up two directory levels. The header becomes ../../partials/header and the footer becomes ../../partials/footer. Include paths must always match your view's actual location relative to the partials directory.

To make your contact form easier to access during development and testing, add navigation links to your header partial. Open src/views/partials/header.ejs and locate your existing navigation list. Add the following list items inside your <nav><ul> structure, alongside your other navigation links:


        <li><a href="/contact">Contact</a></li>
        <li><a href="/contact/responses">Contact Responses</a></li>
    

The first link provides easy access to the contact form for users, while the second link allows you to quickly view submissions during development. In a production application, you would typically protect the responses page with authentication so that only administrators can view submissions. For now, leaving it accessible makes testing and development more convenient.

9. Test the Form Functionality

Start your server and thoroughly test your contact form feature. Focus on verifying that the server-side validation and database operations work correctly.

First, test successful submissions:

  1. Visit http://127.0.0.1:3000/contact to see the form.
  2. Submit a message with both subject and message fields properly filled out.
  3. Check your server console for the success message.
  4. Visit http://127.0.0.1:3000/contact/responses to verify your submission appears in the database.

Next, test the server-side validation by intentionally submitting forms that violate the validation rules. Watch your server console for validation error messages:

In each case, the form should redirect back to the contact page without saving invalid data to the database. You should see validation errors logged in your server console that explain why the submission was rejected. Verify in the responses page that invalid submissions do not appear in your database.

Also verify that your CSS loads correctly by inspecting the page source or using browser developer tools to confirm that contact.css is loaded on contact pages but not on other pages like the home page or about page.

10. Add Client-Side Validation

Now add HTML5 validation attributes to improve the user experience by catching validation errors before the form is submitted. Update your src/views/forms/contact/form.ejs file by modifying the input elements to include validation attributes:


        <input type="text" id="subject" name="subject" 
            placeholder="What is this message about?" 
            minlength="2" required>
    

        <textarea id="message" name="message" rows="6" 
            placeholder="Please provide details about your question or feedback..." 
            minlength="10" required></textarea>
    

The required attribute prevents form submission if the field is empty, and minlength enforces the same minimum length requirements as your server-side validation. Test the client-side validation by trying to submit the form with invalid data. The browser should prevent submission and display helpful error messages before the form data is sent to the server.

After adding client-side validation, verify that valid data still processes correctly by submitting a properly filled form and checking that it appears in your responses page. Client-side validation improves user experience, but server-side validation remains essential for security since client-side validation can be bypassed.

The Two-Layer Validation Approach

You might wonder why we implement validation twice, both on the client and server. Each layer serves a different purpose and neither can replace the other.

Client-side validation provides immediate feedback to users, catching obvious errors before they wait for a server response. This improves user experience and reduces unnecessary server requests. However, client-side validation can be bypassed by malicious users or disabled in the browser.

Server-side validation provides security and data integrity. Since it runs on your server, users cannot bypass it. This is your application's last line of defense against invalid or malicious data. Server-side validation is not optional, even when client-side validation is present.

In professional applications, you always implement both layers. Think of client-side validation as a convenience for honest users, and server-side validation as protection against all users, honest or not.

Key Concepts Summary

This assignment introduced several important concepts for building interactive web applications. You learned how to handle POST requests in Express, which are essential for processing form submissions and other user input. The express-validator library provided professional-grade validation tools that ensure data integrity and security.

The MVC architecture you implemented separates concerns effectively: models handle database operations, controllers process requests and coordinate between models and views, and views handle user interface presentation. This organization makes your code more maintainable and easier to extend.

You also implemented both client-side and server-side validation, understanding that client-side validation improves user experience while server-side validation provides security. The SQL file approach you used for database setup follows industry best practices for managing database schema changes and keeps your database code organized and maintainable.

Most importantly, you created a complete feature that accepts user input, validates it, stores it in a database, and provides an interface to view the results. This pattern forms the foundation for many web application features, from user registration to content management systems.

Experiment and Extend

Try adding additional fields to your contact form, such as a name field or email address. Remember to update the database table structure in your practice.sql file using ALTER TABLE statements, add validation rules for the new fields in your routes file, update your model functions to handle the additional columns, and modify your views to display the additional information.

Experiment with different validation rules using express-validator. Try adding email validation with isEmail(), phone number validation with matches() and a regular expression pattern, or custom validation rules that check for specific content requirements using the custom() method.

Consider adding a simple navigation link to your contact form in your site's main navigation, and add a link from the contact form to the responses page so you can easily view submissions during development. You could also experiment with adding a delete button to each response that allows you to remove submissions from the database.