Controllers
A Controller is the server-side entry point for a route. It handles the initial page load, declares actions that the browser can call, and optionally sets up custom HTTP routes and socket events.
Basic shape
from sprag import Controller, Field, Schema, action
class TodoController(Controller):
route = "/todos"
def load(self):
todos = self.service("todos")
return {"items": todos.list_items(), "filter": "all"}
@action(schema=Schema("add_item", {"text": Field(str, required=True)}))
def add_item(self, text):
todos = self.service("todos")
todos.add_item(text)
return {"items": todos.list_items()}
load()
Called on every page request. Returns a dict that becomes:
- The data passed to
Screen.render()for SSR - The
window.__SPRAG_PAYLOAD__sent to the browser for hydration
def load(self):
user = self.request.user
return {"profile": user.profile, "settings": user.settings}
If load() returns a redirect, the server sends a 302 before rendering:
def load(self):
if not self.request.user:
return self.redirect("/login")
return {"user": self.request.user}
Actions
Actions are named server mutations that the browser can call via call_action().
@action(schema=Schema("update", {
"id": Field(int, required=True),
"text": Field(str, required=True),
}))
def update(self, id, text):
# mutate state, return new state
return {"items": updated_items}
The schema validates the incoming payload automatically. Invalid data returns a structured 400 response.
Deferred actions
For work that takes longer than a request cycle:
@action(defer=True)
def process_file(self, file_id):
# This runs in a background greenlet
# The browser gets an immediate acknowledgment
result = expensive_operation(file_id)
return {"status": "done", "result": result}
Request context
Inside any controller method, self.request gives you the current request:
| Property | Description |
|---|---|
self.request.params | URL params (slug, segments, etc.) |
self.request.query | Query string as a dict |
self.request.session | RequestSession helper |
self.request.session_id | Session ID string |
self.request.user | Authenticated user (or None) |
self.request.active_profile | Active auth profile (or None) |
self.request.cookies | Request cookies |
self.request.file(name) | Uploaded file by field name |
self.request.files_list(name) | List of uploaded files |
Auth guards
from sprag import requires_auth
class AdminController(Controller):
route = "/admin"
@requires_auth(redirect_to="/login")
def load(self):
return {"users": get_all_users()}
@requires_auth
@action(schema=Schema("delete_user", {"id": Field(int, required=True)}))
def delete_user(self, id):
remove_user(id)
return {"users": get_all_users()}
Apply @requires_auth at the class level to guard everything, or per-method.
Custom HTTP routes
For endpoints that aren't page loads or actions (APIs, webhooks, etc.):
def build_routes(self, router):
@router.route("/api/health")
def health():
return {"status": "ok"}
@router.route("/api/items", methods=["POST"])
def create_item():
data = self.request.body
return {"created": True}
Socket events
For handling real-time WebSocket events:
def build_events(self, handler):
handler.on("chat_message", self._on_chat)
def _on_chat(self, data, session_id=None):
# Process incoming socket event
self.emit_socket("new_message", {
"text": data["text"],
"from": session_id,
})
Pushing to the browser
# Broadcast to all connected clients
self.emit_socket("update", {"count": new_count})
# Target a specific session
self.emit_socket("notification", {"msg": "Done!"}, session_id=sid)
# Target a topic (room)
self.emit_socket("room_update", data, topic="room:123")
Redirects
# From load()
def load(self):
return self.redirect("/somewhere")
# From an action
@action(schema=Schema("submit", {...}))
def submit(self, **data):
save(data)
return self.redirect("/success")
The browser follows redirects automatically when using call_action().