Color Mode

Implementing Flash Messages

In this assignment, you will implement a flash message system that allows your application to display temporary feedback messages to users after form submissions and redirects. Flash messages solve a fundamental problem in web development: how to provide user feedback after a redirect. When a user submits a form and your server redirects them to another page, you need a way to communicate success or error information across that redirect.

You will create custom middleware that manages flash message storage and retrieval using sessions, integrate this middleware into your application, and update your existing form controllers (contact, registration, and login) to use flash messages instead of console logging. By the end of this assignment, your users will see clear, helpful feedback messages displayed directly in the browser instead of hidden in server logs.

Understanding the Flash Message Pattern

Before implementing flash messages, you need to understand the problem they solve. Consider what happens when a user submits a form on your website. The browser sends a POST request to your server with the form data. Your server validates the data and processes it. If validation fails, you want to tell the user what went wrong. If validation succeeds, you want to confirm the action was completed.

The challenge arises from the Post-Redirect-Get pattern, a web development best practice that prevents duplicate form submissions. After processing a POST request, your server sends a redirect response (status 302 or 303) that tells the browser to make a new GET request to a different URL. This redirect prevents users from accidentally resubmitting the form if they refresh the page.

However, redirects create a problem for user feedback. The POST request and the subsequent GET request are completely separate HTTP requests with separate request and response objects. You cannot simply attach data to the response because the redirect discards it. You need a way to store the message during the POST request and retrieve it during the GET request.

Flash messages solve this problem by storing temporary messages in the session. The session persists across requests because it is identified by a cookie that the browser sends with every request to your server. During the POST request, you store the message in the session. During the redirect GET request, you retrieve the message from the session and display it to the user. After displaying the message once, you delete it from the session so it does not appear again on subsequent page loads.

Why Sessions Are Required

Flash messages depend on session storage because sessions are the only built-in mechanism in Express that persists data across separate HTTP requests for the same user. Your application already has session middleware configured from the login assignment, so flash messages integrate seamlessly into your existing authentication system.

Assignment Instructions

1. Create the Flash Middleware

Create a new file at src/middleware/flash.js. This middleware will provide the core functionality for storing and retrieving flash messages. Add the following code:


        /**
         * Flash Message Middleware
         * 
         * Provides temporary message storage that survives redirects but is consumed on render.
         * Messages are stored in the session and organized by type (success, error, warning, info).
         * 
         * Usage in controllers:
         *   req.flash('success', 'Message text')  // Store a message
         *   req.flash('error')                    // Get all error messages
         *   req.flash()                           // Get all messages (all types)
         */

        /**
         * Initialize flash message storage and provide access methods
         */
        const flashMiddleware = (req, res, next) => {
            // Track if flash messages were set (need to save session before redirect)
            let sessionNeedsSave = false;
            
            // Override res.redirect to save session before redirecting
            const originalRedirect = res.redirect.bind(res);
            res.redirect = (...args) => {
                if (sessionNeedsSave && req.session) {
                    // Save session before redirecting
                    req.session.save(() => {
                        originalRedirect.apply(res, args);
                    });
                } else {
                    originalRedirect.apply(res, args);
                }
            };
            
            /**
            * The flash function handles both setting and getting messages
            * - Called with 2 args (type, message): stores a new message
            * - Called with 1 arg (type): retrieves and clears messages of that type
            * - Called with 0 args: retrieves and clears all messages
            */
            req.flash = function(type, message) {
                // Guard: If session doesn't exist (e.g., after session.destroy()), 
                // return early to prevent errors. Flash messages require a session to store.
                if (!req.session) {
                    // If setting a message (both type and message provided), can't do without session
                    if (type && message) {
                        return; // Silently fail - no session to store in
                    }
                    // If getting messages, return empty structure
                    return { success: [], error: [], warning: [], info: [] };
                }
                
                // Initialize flash storage if it doesn't exist
                if (!req.session.flash) {
                    req.session.flash = {
                        success: [],
                        error: [],
                        warning: [],
                        info: []
                    };
                }

                // SETTING: Two arguments means we're storing a new message
                if (type && message) {
                    // Ensure this message type's array exists
                    if (!req.session.flash[type]) {
                        req.session.flash[type] = [];
                    }
                    // Add the message to the appropriate type array
                    req.session.flash[type].push(message);
                    // Mark that session needs to be saved before redirect
                    sessionNeedsSave = true;
                    return;
                }

                // GETTING ONE TYPE: One argument means retrieve messages of that type
                if (type && !message) {
                    const messages = req.session.flash[type] || [];
                    // Clear this type's messages after retrieving
                    req.session.flash[type] = [];
                    return messages;
                }

                // GETTING ALL: No arguments means retrieve all message types
                const allMessages = req.session.flash || {
                    success: [],
                    error: [],
                    warning: [],
                    info: []
                };

                // Clear all flash messages after retrieving
                req.session.flash = {
                    success: [],
                    error: [],
                    warning: [],
                    info: []
                };

                return allMessages;
            };

            next();
        };

        /**
        * Make flash function available to all templates via res.locals
        * This middleware must run AFTER flashMiddleware
        */
        const flashLocals = (req, res, next) => {
            // Attach the flash function to res.locals so templates can access it
            // The function is NOT called here, just made available
            // Messages are only consumed when a template calls flash()
            res.locals.flash = req.flash;
            next();
        };

        /**
        * Combined flash middleware that runs both functions in the correct order
        * Import and use this as a single middleware function in your application
        */
        const flash = (req, res, next) => {
            flashMiddleware(req, res, () => {
                flashLocals(req, res, next);
            });
        };

        export default flash;
    

Read through the code and comments carefully. The middleware exports a single flash function that internally runs two middleware functions in sequence. The flashMiddleware function attaches the flash() method to the request object, while flashLocals makes that function available to templates through res.locals.

The req.flash() function behaves differently based on the number of arguments you pass. With two arguments (type and message), it stores a new message in the session. With one argument (type), it retrieves all messages of that type and clears them. With no arguments, it retrieves all messages of all types and clears them. This design provides flexibility for different use cases while keeping the API simple.

Understanding Message Organization

Flash messages are organized by type in the session storage. Each type (success, error, warning, info) has its own array that can hold multiple messages. This organization allows you to display different types of messages with different styling, making it easy for users to distinguish between successful actions, errors, warnings, and general information.

2. Integrate Flash Middleware into Your Application

Open your server.js file and import the flash middleware near the top of the file with your other middleware imports. Add this line after your session imports:


        import flash from './src/middleware/flash.js';
    

Next, register the flash middleware in your application. This middleware must run after session middleware (because it uses sessions) but before your routes (so routes can use req.flash()). Locate where you register your global middleware function and add the flash middleware immediately after it:


        // Global middleware (sets res.locals variables)
        app.use(addLocalVariables);

        // Flash message middleware (must come after session and global middleware)
        app.use(flash);
    

The order matters here. Session middleware must run first to create req.session. Flash middleware runs next to add req.flash() and res.locals.flash. Your routes run last so they can use flash messages.

3. Add Flash Message Display to Your Header Partial

Open src/views/partials/header.ejs and add the flash message display code immediately after the closing </header> tag. This placement ensures flash messages appear at the top of the page content, right below the navigation, making them immediately visible to users.


        </header>

        <% 
            const messages = flash();
            const hasMessages = Object.values(messages).some(arr => arr.length > 0);
        %>

        <% if (hasMessages) { %>
            <div class="flash-messages">
                <% Object.keys(messages).forEach(type => { %>
                    <% messages[type].forEach(message => { %>
                        <div class="flash <%= type %>">
                            <%= message %>
                        </div>
                    <% }); %>
                <% }); %>
            </div>
        <% } %>
    

This template code calls flash() to retrieve all messages, which automatically clears them from the session. It then checks if any messages exist across all types. If messages are found, it loops through each type and displays each message in a div with appropriate CSS classes for styling.

The Object.keys(messages) approach iterates through all message types dynamically, meaning this code will work even if you add new message types in the future. Each message div gets two CSS classes: a general flash class for common styling and a specific type class (success, error, warning, or info) for type-specific styling.

4. Add Flash Message Styles

Open public/css/main.css and add styles for flash messages. These styles should make messages visually distinct, easy to read, and clearly communicate their purpose through color and design.


        /* Flash Messages */
        .flash-messages {
            margin: 1rem auto;
            max-width: 1200px;
            padding: 0 1rem;

            .flash {
                padding: 1rem 1.25rem;
                margin-bottom: 1rem;
                border-radius: 4px;
                border-left: 4px solid;
                font-size: 0.95rem;
                line-height: 1.5;
            
                &.success {
                    background-color: #d4edda;
                    border-color: #28a745;
                    color: #155724;
                }
            
                &.error {
                    background-color: #f8d7da;
                    border-color: #dc3545;
                    color: #721c24;
                }
            
                &.warning {
                    background-color: #fff3cd;
                    border-color: #ffc107;
                    color: #856404;
                }
            
                &.info {
                    background-color: #d1ecf1;
                    border-color: #17a2b8;
                    color: #0c5460;
                }
            }
        }
    

These styles create a consistent look for all flash messages while using distinct colors for each type. The left border provides a visual accent that makes the message type immediately recognizable. The background colors are subtle but noticeable, and the text colors ensure good readability against their backgrounds.

5. Update Contact Form Controller

Open src/controllers/forms/contact.js and replace all console.log() and console.error() statements with flash messages. Locate the handleContactSubmission function. Your current code likely has validation error handling that uses console.error() and a try-catch block that uses console.log(). You need to replace these with flash messages.

Before you begin, change the success redirect to use res.redirect('/contact') instead of res.redirect('/contact/responses'). This ensures that all users (including non-authenticated users) can see the flash message confirming their submission. The flash message now serves as the confirmation, so redirecting to the responses page is no longer necessary.

For validation errors, replace your console logging with this code:


        // Inside your validation error check
        if (!errors.isEmpty()) {
            // Store each validation error as a separate flash message
            errors.array().forEach(error => {
                req.flash('error', error.msg);
            });
            return res.redirect('/contact');
        }
    

For successful submissions, replace your console logging with this code just before the redirect:


        // After successfully saving to the database
        req.flash('success', 'Thank you for contacting us! We will respond soon.');
        res.redirect('/contact');
    

For catch block errors, keep the console.error() for server logging but add a flash message for the user:


        catch (error) {
            console.error('Error saving contact form:', error);
            req.flash('error', 'Unable to submit your message. Please try again later.');
            res.redirect('/contact');
        }
    

Notice the pattern: validation errors loop through all error messages and create separate flash messages, successful submissions create a success flash message, and unexpected errors create an error flash message. The console.error() remains in the catch block because it logs technical details to the server for debugging, while the flash message provides user-friendly feedback to the browser.

Check Your Work

If you want to verify your implementation, here is the complete handleContactSubmission function with flash messages properly integrated:


            const handleContactSubmission = async (req, res) => {
                const errors = validationResult(req);
                
                if (!errors.isEmpty()) {
                    // Store each validation error as a separate flash message
                    errors.array().forEach(error => {
                        req.flash('error', error.msg);
                    });
                    return res.redirect('/contact');
                }
    
                try {
                    const { subject, message } = req.body;
                    await createContactForm(subject, message);
                    req.flash('success', 'Thank you for contacting us! We will respond soon.');
                    res.redirect('/contact');
                } catch (error) {
                    console.error('Error saving contact form:', error);
                    req.flash('error', 'Unable to submit your message. Please try again later.');
                    res.redirect('/contact');
                }
            };
        

Always call req.flash() before res.redirect(). The flash message must be stored in the session before the redirect occurs. If you redirect first, the message will not be available when the next page loads.

6. Update Registration Form Controller

Open src/controllers/forms/registration.js and apply the same flash message pattern you just learned. Just like with the contact form, make sure to change the success redirect to use res.redirect('/login') instead of res.redirect('/register/list'). This ensures that all users (including non-authenticated users) can see the flash message confirming their registration. The flash message now serves as the confirmation, so redirecting to the list page is no longer necessary.

Next, locate the processRegistration function and update it following these requirements:

This controller demonstrates three different message types: error for validation failures, warning for the duplicate email scenario (which is not exactly an error but something the user should be aware of), and success for completed registration.

7. Update Login Form Controller

Open src/controllers/forms/login.js and apply the flash message pattern to the processLogin function. Follow these requirements:

Security Consideration

Notice that both "user not found" and "invalid password" scenarios use the exact same message: "Invalid email or password." This prevents attackers from determining which email addresses have accounts in your system, a security vulnerability called account enumeration. Never reveal whether the email exists or the password was wrong; always use a generic message for authentication failures.

8. Test Your Flash Messages

Restart your server and thoroughly test flash messages across all your forms. Testing requires checking both success and error scenarios to ensure messages display correctly in all situations.

Bypassing Client-Side Validation for Testing

Your forms have HTML5 validation attributes like required, minlength, and type="email" that prevent form submission when fields are invalid. This client-side validation happens in the browser before the request ever reaches your server, making it impossible to test your server-side validation and flash messages with bad data.

To test server-side validation, you need to bypass client-side validation. Add the novalidate attribute to your form's opening tag. For example, in your contact form view, change <form method="POST" action="/contact"> to <form method="POST" action="/contact" novalidate>. This tells the browser to skip HTML5 validation and submit the form directly to your server.

After testing, remove the novalidate attribute. Client-side validation provides immediate feedback to users and reduces unnecessary server requests, so you want it active in production. Use novalidate only during development to test your server-side validation logic.

Contact Form Testing: Add novalidate to your contact form. Submit with valid data and confirm a success message appears. Submit with an empty subject or message and confirm validation error messages appear. Submit with a subject that is too short (less than 2 characters) and verify the error message displays. Refresh the page after seeing a message and confirm it disappears.

Registration Form Testing: Add novalidate to your registration form. Submit with valid, unique credentials and confirm a success message appears on the login page (check that the redirect worked and the message survived). Submit with mismatched emails or passwords and confirm error messages appear for each validation failure. Try registering with an email that already exists and confirm the warning message appears. Submit with a password that is too short and verify the validation error displays.

Login Form Testing: Add novalidate to your login form. Log in with valid credentials and confirm the personalized welcome message appears on the dashboard. Try logging in with an email that does not exist in the database and confirm the "Invalid email or password" message appears. Try logging in with a valid email but wrong password and confirm the same generic error message appears. Submit with an empty password field and confirm the validation error message displays.

Pay attention to the message styling and positioning. Messages should appear at the top of the content area below the navigation, be clearly visible, and use colors that communicate their purpose. Success messages should feel positive (green), errors should be noticeable (red), warnings should be cautionary (yellow), and info messages should be neutral (blue).

Testing Flash Behavior

Flash messages should survive redirects but not page refreshes. After submitting a form and seeing a message, refresh the page. The message should disappear because calling flash() in the template automatically clears messages from the session. This one-time display behavior is what makes them "flash" messages. Navigate away from the page and return. No message should appear because it was already consumed during the first page load.

Key Concepts Summary

This assignment introduced flash messages, a session-based pattern for displaying temporary user feedback across redirects. Flash messages solve the fundamental challenge of the Post-Redirect-Get pattern, where redirects prevent direct data passing between requests.

You created custom middleware that manages flash message storage and retrieval through the session. The middleware provides a single flash() function that both stores and retrieves messages, with retrieval automatically consuming and deleting messages from the session. Messages are organized by type (success, error, warning, info) in arrays, allowing multiple messages of the same type to be displayed together.

You integrated flash message display into your header partial, creating a consistent location for user feedback across all pages without code duplication. The dynamic template logic uses Object.keys() to iterate through message types, making the system extensible to any number of types without code changes.

In your controllers, you replaced console logging with flash messages, improving the user experience by displaying feedback directly in the interface rather than hidden in server logs. You practiced the pattern of storing messages before redirecting and confirmed that messages survive redirects but are consumed on render, preventing them from displaying repeatedly.

Experiment and Extend

Now that you have a working flash message system, consider these enhancements to deepen your understanding:

Conclusion

Flash messages are a fundamental pattern in web applications that follow the Post-Redirect-Get approach. By storing temporary messages in sessions, they bridge the gap between form processing and user feedback, ensuring users understand the outcomes of their actions. The pattern is simple, reliable, and works without requiring JavaScript, making it an essential tool for creating responsive, user-friendly web applications.

The middleware-based implementation you created demonstrates important Express patterns: custom middleware for request augmentation, res.locals for template data, and session storage for persisting state across requests. These patterns apply beyond flash messages to many other features you will build as you continue developing web applications.