Skip to content

Flash Messages

Flash messages are short notices a handler sets before a redirect — “Saved!”, “Could not delete”, etc. — that the next request reads and displays. After the next request, the message is gone automatically. It’s the classic pattern for post-redirect form feedback.

Keys are arbitrary strings — use whatever name makes sense for your app. Common ones are "success", "error", "notice", but nothing stops you from using "cart_added", "payment_failed", "onboarding_step_done".

OptionTypeDefaultDescription
(none)Flash takes an empty FlashConfig today; behavior is governed by the surrounding session.

Flash rides on the existing session middleware, so the session cookie lifetime controls how long an unread flash could theoretically survive (though they clear on first read). Messages are stored in the same in-memory assigns-backed session store — nothing hits the database.

  1. Register session and flash in the pipeline — in that order, because flash reads from values the session middleware loaded into ctx.assigns.

    const App = pidgn.Router.define(.{
    .middleware = &.{
    pidgn.session(.{}),
    pidgn.flash(.{}),
    },
    .routes = &.{
    pidgn.Router.post("/posts", createPost),
    pidgn.Router.get("/posts/:id", showPost),
    },
    });
  2. Set a flash before redirecting — call ctx.putFlash(key, message) in the handler that finishes the write, then redirect.

    fn createPost(ctx: *pidgn.Context) !void {
    // ... save the post ...
    try ctx.putFlash("success", "Post created");
    ctx.redirect("/posts/42", .see_other);
    }
  3. Read it on the next request — in the redirect target, ctx.getFlash(key) returns the message once, then it’s gone.

    fn showPost(ctx: *pidgn.Context) !void {
    const notice = ctx.getFlash("success"); // optional
    try ctx.render(ShowTemplate, .ok, .{
    .flash_success = notice orelse "",
    // ... other template fields
    });
    }

Any string works. Some common patterns:

try ctx.putFlash("success", "Saved!");
try ctx.putFlash("error", "Could not connect");
try ctx.putFlash("notice", "Your session has expired");
try ctx.putFlash("warning", "This profile is public");
// App-specific keys are fine too:
try ctx.putFlash("cart_added", "Item added to your cart");
try ctx.putFlash("payment_failed", "Your card was declined");
try ctx.putFlash("onboarding_done", "You're all set!");

ctx.putFlash returns an error (it allocates the compound key from the request arena), so remember the try.

The handler is responsible for piping the flash into the template’s data struct — templates don’t reach into ctx.assigns directly. Render with a conditional:

{{#if flash_success}}
<div class="alert alert-success">{{flash_success}}</div>
{{/if}}
{{#if flash_error}}
<div class="alert alert-error">{{flash_error}}</div>
{{/if}}

A small helper keeps handlers tidy:

fn withFlash(ctx: *pidgn.Context, comptime keys: []const []const u8, base: anytype) @TypeOf(base) {
var out = base;
inline for (keys) |k| {
@field(out, "flash_" ++ k) = ctx.getFlash(k) orelse "";
}
return out;
}
// usage:
try ctx.render(Tmpl, .ok, withFlash(ctx, &.{"success", "error"}, .{
.title = "Home",
.flash_success = "",
.flash_error = "",
// ...
}));

The middleware maintains two naming conventions on top of the session store:

  • __flash_<key> — the pending value. Written by putFlash and persisted to the session so it survives the redirect.
  • flash_<key> — the display value for the current request. Populated by the middleware at the start of each request (from the pending entry), cleared at the end so it doesn’t accumulate into the next request.

On each request, flash scans ctx.assigns for keys starting with __flash_, promotes each to its flash_<key> counterpart, zeroes the pending entry (so it doesn’t survive past this request), then runs the handler. After the handler, it clears the display keys so they don’t get persisted back into the session. Net effect: a pending flash becomes visible on exactly one subsequent request.