Understanding Flash Messages in Web Applications
Flash messages are a fundamental pattern in web development that solve a specific problem: how do you display temporary feedback to users after processing a form submission or completing an action that requires a redirect? This reading explores the challenge flash messages address, how they work, and why they are essential for creating responsive, user-friendly web applications.
The Problem: Losing Messages After Redirects
When building web applications with forms, you frequently encounter a common workflow: a user submits a form, the server processes it, and then redirects the user to another page. This pattern, known as Post-Redirect-Get (PRG), is a best practice because it prevents duplicate form submissions if users refresh their browser.
However, this pattern creates a significant challenge. Imagine a user registers for an account. Your server validates the data, creates the account in the database, and then redirects to the login page. How do you tell the user their registration was successful? You cannot simply pass a variable like success: true because the redirect causes a completely new HTTP request. Any data you attached to the response from the registration handler is lost when the browser makes the new GET request to the login page.
Consider this broken approach:
// Registration controller
app.post('/register', async (req, res) => {
// Validate and create user...
// This won't work! The redirect loses this data
res.redirect('/login', { message: 'Registration successful!' });
});
The redirect() method does not accept a second parameter for passing data. Even if it did, redirects work by sending a 302 status code that tells the browser to make a new request to a different URL. That new request has no knowledge of the previous request or its data.
You might think to pass the message as a query parameter:
res.redirect('/login?message=Registration+successful!');
While this technically works, it creates several problems. First, the message appears in the URL, which looks unprofessional and confusing to users. Second, if users bookmark the page or share the URL, the message will display again inappropriately. Third, sensitive information like error details should never be exposed in URLs where they can be logged by browsers, proxies, and servers.
The Solution: Session-Based Temporary Storage
Flash messages solve this problem by using session storage as a temporary holding area for messages. The concept is elegant: when your controller needs to display a message after a redirect, it stores the message in the user's session before redirecting. When the next page renders, it retrieves the message from the session, displays it to the user, and immediately deletes it from the session.
This "flash" behavior is where the name comes from. The message exists briefly, displays once, and then disappears, much like a camera flash. The message survives the redirect because sessions persist across multiple HTTP requests, but it does not survive beyond a single display because the system explicitly removes it after rendering.
The lifecycle of a flash message follows this sequence:
-
User submits a form to
/register - Controller processes the form and stores a success message in the session
-
Controller redirects to
/login -
Browser makes a new GET request to
/login - Login controller renders the login page
- During rendering, the template retrieves flash messages from the session
- The act of retrieving the messages deletes them from the session
- User sees the success message on the login page
- If user refreshes, no message appears because it was already consumed
How Sessions Enable Flash Messages
To understand flash messages, you must first understand how sessions work in Express applications. A session is server-side storage associated with a specific user. When a user first visits your application, Express creates a unique session identifier, stores it in a cookie on the user's browser, and maintains session data on the server.
On every subsequent request, the browser sends the session cookie, allowing Express to retrieve that user's session data. This persistence across requests is what makes flash messages possible. When you store a message in req.session, that data remains available for future requests from the same user until you explicitly delete it or the session expires.
In your applications, you have configured Express to store session data in PostgreSQL using the connect-pg-simple package. This means session data persists even if your server restarts, and multiple server instances can share the same session store. The database approach is more robust than storing sessions in memory, which is lost when the server stops.
Your server configures sessions with a 24-hour maximum age, meaning session data (including flash messages) can persist for up to a day. However, flash messages are explicitly deleted after being displayed, so they never reach this timeout. The session cookie uses httpOnly for security, preventing JavaScript from accessing the session identifier, and secure in production to require HTTPS.
The Flash Message Data Structure
A well-designed flash message system needs to handle multiple types of messages (success, error, warning, informational) and potentially multiple messages of the same type. The data structure that best supports this is an object where each property represents a message type, and each value is an array of message strings.
// Structure stored in req.session.flash
{
success: ['Registration complete!', 'Welcome email sent!'],
error: ['Invalid password format'],
warning: [],
info: []
}
This structure offers several advantages. First, it organizes messages by type, making it easy to style them differently in your templates. Success messages might display with a green background, while errors use red. Second, the array structure naturally supports multiple messages of the same type. If form validation finds three errors, you can store all three and display them together. Third, the structure is extensible. If you later need a new message type like debug or notice, you can add it without changing how the system works.
The API for working with this structure is deliberately simple. To add a message, you call a function with the type and message text. To retrieve messages, you call the same function with just the type, or with no arguments to get all messages at once. The function handles the complexity of creating arrays, appending messages, and cleaning up empty arrays.
Implementing Flash Messages with Middleware
The flash message system requires two pieces of middleware. The first middleware, which you add early in your middleware stack, initializes the flash storage structure and provides methods for setting and getting messages. The second middleware, which typically runs as part of your global middleware, makes the flash function available to all templates.
Flash Storage Middleware
The storage middleware attaches a flash() method to the request object. This method handles both storing and retrieving messages, using different behavior based on how many arguments it receives.
When called with two arguments (type and message), it stores a new message. The middleware first ensures that req.flash exists and is properly initialized as an object with empty arrays for each message type. It then pushes the new message onto the appropriate type's array. This lazy initialization approach means the flash storage only exists when needed, keeping sessions lean.
When called with one argument (just type), it retrieves all messages of that type, removes them from the session, and returns them as an array. This retrieval-and-deletion behavior is the "flash" mechanism. The messages are consumed by being read, ensuring they only display once.
When called with no arguments, it retrieves all messages of all types, clears the entire flash storage, and returns an object containing all the messages organized by type. This is the most common usage in templates because it allows a single call to get everything that needs to be displayed.
You might wonder why flash messages require middleware rather than a simple utility function. The answer is that middleware has access to both req and res objects and runs automatically on every request. This allows the flash system to initialize storage lazily, attach methods to the request object, and make itself available to templates through res.locals without requiring manual setup in every controller.
Template Access Middleware
The second piece of middleware is simpler but equally important. It makes the req.flash function available to all templates by attaching it to res.locals. The res.locals object is a special Express feature that automatically makes its properties available as variables in all rendered templates.
This middleware must run after the flash storage middleware (so req.flash exists) but before any routes render templates. Typically, you place it in your global middleware file along with other template utilities like copyright year or current user information.
An important detail about this middleware is that it only makes the function available to templates. It does not call the function, which means it does not consume the messages. The messages remain in the session until a template actually renders and calls flash(). This is crucial for the redirect scenario. When a controller redirects, the template access middleware runs, but no template renders, so the messages survive. Only when the redirected-to route renders its template do the messages get consumed and deleted.
Using Flash Messages in Controllers
From a controller's perspective, using flash messages is straightforward. After processing a form or completing an action, and before redirecting, call req.flash() with the message type and text. The message is stored in the session automatically, surviving the redirect.
app.post('/register', async (req, res) => {
try {
// Validate form data
const errors = validateRegistration(req.body);
if (errors.length > 0) {
errors.forEach(err => req.flash('error', err));
return res.redirect('/register');
}
// Create user account
await createUser(req.body);
// Success message
req.flash('success', 'Registration complete! You can now log in.');
res.redirect('/login');
} catch (err) {
req.flash('error', 'An unexpected error occurred. Please try again.');
res.redirect('/register');
}
});
Notice how the controller can add multiple error messages in a loop, and each one is stored separately. The controller does not need to worry about how the messages are displayed or when they are cleared. It simply stores the messages and redirects, trusting the template to handle presentation.
Controllers that render views directly (without redirecting) can still use flash messages, but they are less common in those situations. If you are already rendering a view, you can pass error or success data directly to the template. Flash messages shine specifically in the redirect scenario where direct data passing is impossible.
Displaying Flash Messages in Templates
Templates retrieve and display flash messages by calling the flash() function made available through res.locals. The most flexible approach is to call it with no arguments, receiving an object containing all message types and their arrays.
Because you do not know in advance which message types exist or which have content, the template must dynamically iterate through the object's properties. For each message type that has at least one message, the template renders an appropriately styled container and loops through the messages array.
<%- include('partials/header') %>
<main>
<% const messages = flash(); %>
<% Object.keys(messages).forEach(type => { %>
<% if (messages[type].length > 0) { %>
<div class="alert <%= type %>">
<% messages[type].forEach(msg => { %>
<p><%= msg %></p>
<% }); %>
</div>
<% } %>
<% }); %>
<!-- Rest of page content -->
</main>
<%- include('partials/footer') %>
This template code demonstrates several EJS techniques. The Object.keys() method extracts the property names from the messages object, giving an array of message types. The outer loop iterates through these types, and the length check ensures empty arrays do not render unnecessary HTML. The class name uses the type dynamically, so a success message gets class="alert success" while an error gets class="alert error". The inner loop displays each message as a paragraph.
The critical line is const messages = flash();. This single call retrieves all messages from the session and simultaneously deletes them. After this line executes, req.flash is cleared. If the user refreshes the page, causing another render, the flash storage is empty and no messages display.
The Post-Redirect-Get Pattern
Flash messages are intrinsically tied to the Post-Redirect-Get (PRG) pattern, which is a web development best practice for handling form submissions. Understanding why this pattern exists helps clarify why flash messages are necessary.
When a user submits a form, the browser sends a POST request with the form data. If your controller processes this POST request and directly renders a success page, the browser's URL bar still shows the form's URL, and the browser remembers this as a POST request. If the user refreshes the page, the browser warns them about resubmitting the form, and if they confirm, your server processes the form again. This can lead to duplicate submissions, charges, or database entries.
The PRG pattern solves this by having the POST handler redirect to a different URL after processing. The browser makes a new GET request to this URL, and the URL bar updates. Now if the user refreshes, only the GET request repeats, which is safe and idempotent. The form data is not resubmitted.
However, this pattern necessitates flash messages. The POST handler processes the form and determines the outcome (success or validation errors). It needs to communicate this outcome to the user, but it cannot render directly because that would break the PRG pattern. Instead, it stores outcome messages in the flash storage and redirects. The GET handler then renders the page, and the template displays the flash messages.
A common beginner mistake is to render a template directly from a POST handler instead of redirecting. While this appears to work initially, it creates the duplicate submission problem and provides a poor user experience. Always follow the PRG pattern: POST handlers should process data and redirect, while GET handlers should render views. Flash messages bridge this gap by carrying feedback across the redirect.
Security and Best Practices
Flash messages, while simple in concept, require careful handling to avoid security and usability issues. Several best practices help ensure your flash message implementation is robust and secure.
First, always escape flash message content when displaying it in templates. Use EJS's <%= %> syntax rather than <%- %>. Flash messages often contain user input, such as validation errors that include the data the user submitted. Without proper escaping, a malicious user could inject HTML or JavaScript into error messages, leading to cross-site scripting (XSS) vulnerabilities.
Second, be mindful of what information you include in flash messages. Avoid exposing sensitive data, internal error details, or system information that could help an attacker. Error messages should be helpful to users but vague enough that they do not reveal implementation details. For example, when a login fails, use a generic message like "Invalid credentials" rather than "Password incorrect" or "Email not found," which would help an attacker enumerate valid accounts.
Third, ensure your session configuration is secure. Sessions must use HTTPS in production (enforced by the secure: true cookie setting), use the httpOnly flag to prevent JavaScript access to the session cookie, and use a strong, random session secret that is never committed to version control. Since flash messages rely entirely on sessions, a compromised session allows an attacker to manipulate or read messages.
Finally, consider the user experience. Flash messages should be noticeable but not intrusive. They should clearly indicate their type (success, error, etc.) through both color and text, accommodating users who are colorblind. If a page displays multiple messages, ensure they are all visible without scrolling if possible. Consider using client-side JavaScript to add a dismiss button or auto-hide messages after a few seconds, though this is an enhancement rather than a requirement.
Alternative Approaches and Their Trade-offs
While session-based flash messages are the standard approach in server-rendered applications, other methods exist for communicating feedback to users. Understanding these alternatives and their limitations helps clarify why flash messages are the preferred solution.
One alternative is using query parameters to pass messages in the redirect URL. As discussed earlier, this approach has significant drawbacks. Messages appear in the URL bar, creating a poor user experience. They persist if the user bookmarks or shares the URL. They are logged by servers and proxies, potentially exposing sensitive information. They have length limitations that prevent detailed error messages. For these reasons, query parameter messages should be avoided in favor of flash messages.
Another alternative is client-side storage using JavaScript and localStorage or sessionStorage. After submitting a form, the client stores the outcome in browser storage, redirects, and then retrieves and displays the message. This approach works but has limitations. It requires JavaScript, so users with JavaScript disabled see no feedback. It is more complex to implement, requiring client-side code in addition to server logic. It is harder to secure against XSS attacks. Most importantly, it couples your application to client-side state, making server-side rendering more complex.
In single-page applications (SPAs) that use client-side routing, flash messages work differently. Since navigation does not trigger full page loads, you can store messages in client-side state management systems like Redux or React Context. However, even SPAs often need server-side flash messages for scenarios like email verification links or OAuth callbacks where the server redirects to the client application.
The session-based flash message approach balances simplicity, security, and reliability. It works without JavaScript, keeps the URL clean, stores messages server-side where they are more secure, and integrates seamlessly with the PRG pattern. These advantages make it the standard choice for server-rendered web applications.
Real-World Applications of Flash Messages
Flash messages appear throughout web applications in various contexts, all following the pattern of providing feedback after an action that requires a redirect. Understanding these common use cases helps you recognize when to apply flash messages in your own projects.
Form validation is perhaps the most common use case. When a user submits a form with invalid data, the server validates the input, stores error messages explaining what is wrong, and redirects back to the form. The form redisplays with the error messages, allowing the user to correct their input. Multiple validation errors can be displayed together, each as a separate flash message of type "error."
Account operations frequently use flash messages. After a user registers, logs in, logs out, changes their password, or updates their profile, the server redirects to an appropriate page with a success message. These messages confirm that the action completed successfully and, in the case of logout or password changes, provide security reassurance.
E-commerce applications use flash messages throughout the shopping experience. Adding an item to the cart, updating quantities, removing items, applying coupon codes, and completing checkout all involve form submissions followed by redirects with feedback messages. These messages help users understand that their actions had the intended effect.
Administrative interfaces rely heavily on flash messages for content management actions. Creating, updating, or deleting records typically involves a POST request followed by a redirect back to a listing page or detail view, with a flash message confirming the action. This pattern keeps the interface responsive and informative without requiring JavaScript.
Error handling uses flash messages to communicate problems without exposing users to raw error pages. If a database query fails, a file upload is too large, or an external API is unavailable, the controller can catch the error, store a user-friendly error message, and redirect to a safe page. The user sees helpful feedback rather than a stack trace or generic 500 error page.
Conclusion
Flash messages are a fundamental pattern for providing user feedback in web applications that follow the Post-Redirect-Get pattern. They solve the challenge of communicating information across redirects by temporarily storing messages in session storage, displaying them once, and then removing them. This "flash" behavior ensures users receive important feedback about their actions without cluttering the URL or requiring JavaScript.
The implementation relies on session middleware to persist data across requests, custom middleware to provide a convenient API for storing and retrieving messages, and template logic to dynamically display messages organized by type. The data structure of an object containing arrays of messages by type provides flexibility to handle any number of message types and multiple messages of the same type.
Understanding flash messages requires understanding the broader context of web application architecture: how HTTP is stateless, how sessions provide continuity, why redirects prevent duplicate submissions, and how middleware can augment request and response objects with additional functionality. Flash messages demonstrate how simple patterns, carefully implemented, solve complex problems in ways that are secure, maintainable, and user-friendly.
As you implement flash messages in your own applications, remember that they are a tool for enhancing user experience. The messages should be clear, actionable, and appropriately styled. They should appear when needed and disappear when consumed. They should complement your application's functionality without becoming a distraction. Mastering flash messages is a step toward building web applications that communicate effectively with their users.