Integrate Session with Crystal's built-in HTTP::Server for lightweight applications.
Overview
Basic Setup
1. Define Session Data
require"session"classUserSession < Session::Baseproperty user_id :Int64?property username :String?property role :String="guest"end
2. Configure Session
Session.configure do |config|config.secret=ENV["SESSION_SECRET"]config.timeout=24.hoursconfig.session_key="_myapp_session"config.store=Session::MemoryStore(UserSession).newend
3. Use the Built-in Handler
Session provides a ready-to-use SessionHandler:
4. Access Sessions in Your Handler
Custom Session Handler
If you need custom behavior, implement your own handler:
Complete Example
Production Configuration
Handler Order
Place the session handler early in the chain, after request tracking but before your application logic:
require "http/server"
store = Session.config.store.not_nil!
server = HTTP::Server.new([
Session::SessionHandler.new(store),
MyAppHandler.new,
])
server.listen(8080)
puts "Server listening on http://localhost:8080"
class MyAppHandler
include HTTP::Handler
def call(context : HTTP::Server::Context)
store = Session.config.store.not_nil!
case context.request.path
when "/login"
handle_login(context, store)
when "/profile"
handle_profile(context, store)
when "/logout"
handle_logout(context, store)
else
context.response.print "Hello! <a href='/login'>Login</a>"
end
end
private def handle_login(context, store)
# Create or regenerate session
store.regenerate_id
store.current_session.user_id = 1
store.current_session.username = "alice"
store.current_session.role = "admin"
store.flash["notice"] = "Welcome back!"
context.response.status = HTTP::Status::FOUND
context.response.headers["Location"] = "/profile"
end
private def handle_profile(context, store)
if store.current_session.user_id
notice = store.flash.now["notice"]?
html = <<-HTML
#{notice ? "<p style='color:green'>#{notice}</p>" : ""}
<h1>Profile</h1>
<p>User: #{store.current_session.username}</p>
<p>Role: #{store.current_session.role}</p>
<a href="/logout">Logout</a>
HTML
context.response.print html
else
context.response.status = HTTP::Status::FOUND
context.response.headers["Location"] = "/login"
end
end
private def handle_logout(context, store)
store.delete
store.flash["notice"] = "You have been logged out"
context.response.status = HTTP::Status::FOUND
context.response.headers["Location"] = "/"
end
end
class CustomSessionHandler
include HTTP::Handler
def initialize(@store : Session::MemoryStore(UserSession))
end
def call(context : HTTP::Server::Context)
# Load session from request cookies
@store.load_from(context.request.cookies)
begin
call_next(context)
ensure
# Always set session cookie in response
@store.set_cookies(context.response.cookies)
end
rescue Session::SessionExpiredException
handle_expired_session(context)
rescue Session::SessionCorruptionException
handle_corrupted_session(context)
end
private def handle_expired_session(context)
@store.create
call_next(context)
@store.set_cookies(context.response.cookies)
end
private def handle_corrupted_session(context)
@store.create
@store.flash["error"] = "Your session was invalid. Please log in again."
call_next(context)
@store.set_cookies(context.response.cookies)
end
end
require "http/server"
require "session"
# Define session structure
class UserSession < Session::Base
property user_id : Int64?
property username : String?
property cart_items : Array(Int64) = [] of Int64
end
# Configure
Session.configure do |config|
config.secret = ENV.fetch("SESSION_SECRET", "dev-secret-change-in-production!")
config.timeout = 1.hour
config.sliding_expiration = true
config.store = Session::MemoryStore(UserSession).new
end
store = Session.config.store.not_nil!
# Application handler
class AppHandler
include HTTP::Handler
def call(context : HTTP::Server::Context)
store = Session.config.store.not_nil!
case {context.request.method, context.request.path}
when {"GET", "/"}
render_home(context, store)
when {"POST", "/cart/add"}
add_to_cart(context, store)
when {"GET", "/cart"}
show_cart(context, store)
else
context.response.status = HTTP::Status::NOT_FOUND
context.response.print "Not Found"
end
end
private def render_home(context, store)
cart_count = store.current_session.cart_items.size
context.response.content_type = "text/html"
context.response.print <<-HTML
<h1>Shop</h1>
<p>Cart: #{cart_count} items</p>
<form method="POST" action="/cart/add">
<input type="hidden" name="item_id" value="#{rand(1000)}">
<button>Add Random Item</button>
</form>
<a href="/cart">View Cart</a>
HTML
end
private def add_to_cart(context, store)
if item_id = context.request.form_params["item_id"]?
store.current_session.cart_items << item_id.to_i64
store.flash["notice"] = "Item added to cart!"
end
context.response.status = HTTP::Status::FOUND
context.response.headers["Location"] = "/"
end
private def show_cart(context, store)
items = store.current_session.cart_items
context.response.content_type = "text/html"
context.response.print <<-HTML
<h1>Your Cart</h1>
<ul>
#{items.map { |id| "<li>Item ##{id}</li>" }.join}
</ul>
<a href="/">Continue Shopping</a>
HTML
end
end
# Start server
server = HTTP::Server.new([
Session::SessionHandler.new(store),
AppHandler.new,
])
address = server.bind_tcp(8080)
puts "Listening on http://#{address}"
server.listen