Mastering Flash Messages In PRG Pattern

Mastering Flash Messages In PRG Pattern

Build better UX with Go! Master the Post/Redirect/Get (PRG) pattern and implement safe, read-once Flash messages in Golang. Step-by-step code tutorial.

When building web applications with standard HTML forms, user experience often hinges on two crucial details: preventing duplicate form submissions and providing clear, immediate feedback.

If you render a response directly from an HTML POST request, your users run into a common annoyance: refreshing the page triggers a scary browser warning asking to re-submit form data. Even worse, accidental re-submissions can duplicate database entries or charge a customer twice.

The solution is a classic web architecture pattern known as Post/Redirect/Get (PRG) paired with Flash Messages. In this guide, we'll explore how the PRG pattern works, why Flash messages are essential, and how to implement a clean, thread-safe Flash message system from scratch in idiomatic Go (Golang).


What is the Post/Redirect/Get (PRG) Pattern?

The Post/Redirect/Get (PRG) pattern is a web development design strategy that prevents duplicate form submissions and keeps your browser history clean.

Instead of rendering HTML directly in response to a POST request, the server returns an HTTP redirect response (usually 303 See Other or 302 Found). The browser then follows this redirect by sending a GET request to display the target page.

+---------+              +--------+
| Browser |              | Server |
+---------+              +--------+
     |                        |
     |--- 1. POST /login ---->| (Processes data, creates Flash message)
     |                        |
     |<-- 2. 303 Redirect ----| (Redirects to /dashboard)
     |                        |
     |--- 3. GET /dashboard ->| (Reads & clears Flash message)
     |                        |
     |<-- 4. 200 OK (HTML) ---| (Renders page with feedback)
     v                        v

Why PRG is Essential:

  • Prevents Duplicate Submissions: Hitting F5 or Refresh only repeats the final GET request, not the state-changing POST.
  • Bookmarking & Sharing: Users can safely bookmark or share the resulting page URL without re-triggering server logic.
  • Clean Browser History: The back button behaves predictably without warning prompts.

While the PRG pattern solves the double-submission issue, it introduces a new challenge: HTTP is stateless.

When the server redirects the browser from a POST route to a GET route, any context about the operation (such as "Account created successfully" or "Invalid password") is lost.

A Flash Message is a temporary notification stored in session memory or an encrypted cookie. It follows a read-once lifecycle:

  1. Created during the POST handler execution.
  2. Stored temporarily (in a cookie or server session).
  3. Retrieved during the subsequent GET request.
  4. Deleted immediately after being rendered to the user.

Implementing Flash Messages in Go

Let's build a working Go application that implements the PRG pattern using encrypted browser cookies for Flash messages. We'll use the popular gorilla/sessions package to handle secure cookie storage.

Prerequisites

Initialize a new Go module and install the Gorilla Sessions package:

go mod init go-flash-demo
go get github.com/gorilla/sessions

Step 1: Complete Go Server Implementation

Create a file named main.go:

package main

import (
	"html/template"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/sessions"
)

// Initialize the cookie store with a secret key for encryption.
// In production, load this key from a secure environment variable.
var store = sessions.NewCookieStore([]byte(getEnv("SESSION_KEY", "super-secret-key-32-bytes-long!")))

const flashSessionName = "flash-session"

func main() {
	http.HandleFunc("/", handleHome)
	http.HandleFunc("/submit", handleSubmit)
	http.HandleFunc("/dashboard", handleDashboard)

	log.Println("Server starting on http://localhost:8080...")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

// Helper function to set a flash message in the cookie session.
func setFlash(w http.ResponseWriter, r *http.Request, name string, value string) {
	session, _ := store.Get(r, flashSessionName)
	session.AddFlash(value, name)
	if err := session.Save(r, w); err != nil {
		http.Error(w, "Failed to save session", http.StatusInternalServerError)
	}
}

// Helper function to retrieve and clear flash messages.
func getFlash(w http.ResponseWriter, r *http.Request, name string) []string {
	session, _ := store.Get(r, flashSessionName)
	flashes := session.Flashes(name)

	// Saving the session after reading flashes clears them automatically.
	if err := session.Save(r, w); err != nil {
		log.Printf("Failed to clear flash session: %v", err)
	}

	var result []string
	for _, f := range flashes {
		if str, ok := f.(string); ok {
			result = append(result, str)
		}
	}
	return result
}

// 1. GET / - Renders the initial form
func handleHome(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	renderTemplate(w, "templates/index.html", nil)
}

// 2. POST /submit - Processes form data and executes Redirect
func handleSubmit(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	username := r.FormValue("username")

	// Basic validation
	if username == "" {
		setFlash(w, r, "error", "Username cannot be empty!")
		http.Redirect(w, r, "/", http.StatusSeeOther) // Redirect back to home
		return
	}

	// Business logic execution (e.g., database save)
	log.Printf("Successfully registered user: %s", username)

	// Set success notification and redirect using PRG pattern
	setFlash(w, r, "success", "Welcome aboard, "+username+"! Your account is active.")
	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

// 3. GET /dashboard - Renders page with flash messages
func handleDashboard(w http.ResponseWriter, r *http.Request) {
	successMessages := getFlash(w, r, "success")

	data := map[string]interface{}{
		"Successes": successMessages,
	}

	renderTemplate(w, "templates/dashboard.html", data)
}

func renderTemplate(w http.ResponseWriter, tmplFile string, data interface{}) {
	tmpl, err := template.ParseFiles(tmplFile)
	if err != nil {
		http.Error(w, "Template parsing error: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if err := tmpl.Execute(w, tmpl); err != nil {
		http.Error(w, "Template execution error: "+err.Error(), http.StatusInternalServerError)
	}
}

func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}


Step 2: Creating the HTML Templates

Create a folder named templates and add the following two template files:

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>PRG Pattern in Go</title>
    <style>
        body { font-family: sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
        .alert-error { background-color: #f8d7da; color: #721c24; padding: 10px; margin-bottom: 15px; border-radius: 4px; }
        .form-group { margin-bottom: 15px; }
        input[type="text"] { width: 100%; padding: 8px; box-sizing: border-box; }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
    </style>
</head>
<body>
    <h2>User Registration</h2>

    <form action="/submit" method="POST">
        <div class="form-group">
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" placeholder="Enter name">
        </div>
        <button type="submit">Register</button>
    </form>
</body>
</html>

templates/dashboard.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dashboard</title>
    <style>
        body { font-family: sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
        .alert-success { background-color: #d4edda; color: #155724; padding: 10px; margin-bottom: 15px; border-radius: 4px; }
    </style>
</head>
<body>
    {{range .Successes}}
        <div class="alert-success">{{.}}</div>
    {{end}}

    <h1>Dashboard</h1>
    <p>Welcome to your application dashboard!</p>
    <a href="/">Back to Form</a>
</body>
</html>


Key Best Practices for Flash Messages in Go

  1. Use HTTP 303 See Other for Redirects: When redirecting after a POST request, prefer http.StatusSeeOther (303) over http.StatusFound (302). HTTP 303 explicitly guarantees that the browser will issue a GET request to the target URL regardless of the original method.
  2. Secure Your Cookie Key: Flash cookies must be signed (and ideally encrypted) to prevent client-side tampering. Never hardcode session keys in production source code; always pull them from environment variables or secret vaults.
  3. Keep Messages Small: Cookies have a browser limit of approximately 4KB. Store short string messages or status codes in flash notifications rather than large payload objects.
  4. Category-Based Flashes: Organize flash messages by types (e.g., "success", "error", "warning"). Gorilla's session.AddFlash(value, key) lets you scope messages logically for styled rendering in HTML templates.

Conclusion

Combining the Post/Redirect/Get (PRG) pattern with Flash Messages provides a robust foundation for handling web forms. It prevents accidental double-submits, protects server state integrity, and provides users with immediate, intuitive visual feedback.

With Go's standard library http package and lightweight session managers like gorilla/sessions, implementing clean and secure Flash messages takes only a few lines of idiomatic code.