Sessions and Middleware
When building real applications, you often need to remember things about your users across page loads and requests. Did they log in? What items are in their shopping cart? What theme did they choose? This is where sessions come in.
You might also need to run code before every request, like checking if a user is authenticated or logging requests. That's what middleware is for.
Sessions: Remembering Your Users
A session is a way to store data for a specific user. Each user gets their own session, and the data persists across page navigations and browser refreshes.
Using Sessions
Access the current user's session with ps.session():
@ps.component
def visit_counter():
session = ps.session()
# Get the current visit count, defaulting to 0 if not set
visits = session.get("visits", 0)
# Increment and save
session["visits"] = visits + 1
return ps.div(
ps.h1("Welcome!", className="text-2xl font-bold"),
ps.p(f"You've visited this page {session['visits']} times."),
)Sessions work like dictionaries. You can:
- Get values:
session.get("key")orsession["key"] - Set values:
session["key"] = value - Check for keys:
"key" in session - Delete keys:
del session["key"]
Common Session Use Cases
User authentication:
class AuthState(ps.State):
async def login(self, data: ps.FormData):
username = data.get("username")
password = data.get("password")
# Validate credentials (you'd check a database here)
if self.validate_credentials(username, password):
session = ps.session()
session["user"] = {"username": username}
ps.navigate("/dashboard")
@ps.component
def dashboard():
session = ps.session()
user = session.get("user")
if not user:
ps.navigate("/login")
return ps.div("Redirecting...")
return ps.div(
ps.h1(f"Welcome, {user['username']}!", className="text-2xl"),
ps.button("Logout", onClick=lambda: logout()),
)
def logout():
session = ps.session()
del session["user"]
ps.navigate("/login")User preferences:
@ps.component
def settings_page():
session = ps.session()
theme = session.get("theme", "light")
def set_theme(new_theme):
session["theme"] = new_theme
return ps.div(className=f"theme-{theme}")[
ps.h1("Settings"),
ps.div(
ps.button("Light Theme", onClick=lambda: set_theme("light")),
ps.button("Dark Theme", onClick=lambda: set_theme("dark")),
),
]What to Store in Sessions
Sessions are great for:
- User identity (who is logged in)
- User preferences (theme, language)
- Temporary data (items in a cart before checkout)
- Flash messages (success/error messages shown once)
Avoid storing:
- Large amounts of data (keep sessions lightweight)
- Sensitive data that shouldn't be in memory
- Data that should persist long-term (use a database instead)
Middleware: Code That Runs on Every Request
Middleware lets you run code before a user connects to your app. This is perfect for cross-cutting concerns like authentication, logging, or request validation.
Creating Middleware
Middleware is a class that inherits from ps.PulseMiddleware and implements the connect method:
class LoggingMiddleware(ps.PulseMiddleware):
async def connect(self, *, request, session, next):
print(f"New connection from {request.client.host}")
print(f"Path: {request.url.path}")
# Continue to the next middleware or the actual page
return await next()The connect method receives:
request: Information about the incoming request (URL, headers, etc.)session: The user's session (same asps.session())next: A function to call to continue processing
Authentication Middleware
A common use case is protecting routes that require login:
class AuthMiddleware(ps.PulseMiddleware):
# Routes that don't require authentication
public_routes = ["/", "/login", "/signup", "/about"]
async def connect(self, *, request, session, next):
path = request.url.path
# Allow public routes
if path in self.public_routes:
return await next()
# Check if user is logged in
if not session.get("user"):
# Deny access - user will see an error or be redirected
return ps.Deny()
# User is authenticated, continue
return await next()Using Middleware
Add middleware to your app:
app = ps.App(
routes=[...],
middleware=[
LoggingMiddleware(),
AuthMiddleware(),
],
)Middleware runs in order. Each middleware can:
- Continue: Call
await next()to pass control to the next middleware - Deny: Return
ps.Deny()to reject the connection - Short-circuit: Return early without calling
next()
Multiple Middleware Example
Here's how middleware chains together:
class TimingMiddleware(ps.PulseMiddleware):
async def connect(self, *, request, session, next):
import time
start = time.time()
result = await next()
elapsed = time.time() - start
print(f"Request took {elapsed:.2f}s")
return result
class RateLimitMiddleware(ps.PulseMiddleware):
async def connect(self, *, request, session, next):
# Simple rate limiting (you'd use a proper solution in production)
recent_requests = session.get("recent_requests", 0)
if recent_requests > 100:
return ps.Deny()
session["recent_requests"] = recent_requests + 1
return await next()
app = ps.App(
routes=[...],
middleware=[
TimingMiddleware(), # Runs first
RateLimitMiddleware(), # Runs second
AuthMiddleware(), # Runs third
],
)Combining Sessions and Middleware
Sessions and middleware work together naturally. Middleware can read and modify sessions, making it easy to implement patterns like:
class SessionSetupMiddleware(ps.PulseMiddleware):
async def connect(self, *, request, session, next):
# Initialize session defaults if needed
if "created_at" not in session:
from datetime import datetime
session["created_at"] = datetime.now().isoformat()
session["visits"] = 0
# Track this visit
session["visits"] += 1
session["last_visit"] = datetime.now().isoformat()
return await next()Tips
-
Keep sessions small: Store IDs and references, not large objects.
-
Order middleware carefully: Authentication should usually come early to protect all routes.
-
Use middleware for cross-cutting concerns: If you find yourself checking authentication in every component, use middleware instead.
-
Handle denied connections gracefully: Consider what users see when
ps.Deny()is returned.