Routing ¶
Purpose: Request Delivery — How requests reach handlers based on path, HTTP method and headers.
Routing is the first step in handling requests - it determines which handler function should process an incoming HTTP request based on the path, method, and optionally, headers.
Stario's routing system is built on top of Starlette's router, adding opinionated patterns and conveniences for building hypermedia-driven applications.
Application Setup ¶
The Stario class is the main entry point for your application. It configures routing, middleware, exception handlers, and logging. To some extent it's just a main router, with some extra features.
Quick Start with Decorators (Recommended) ¶
The simplest way to define routes is using decorators on your handler functions:
from stario import Stario
app = Stario()
@app.query("/")
async def index():
return div("Welcome")
@app.query("/users/{user_id}")
async def get_user(user_id: PathParam[int]):
return div(f"User {user_id}")
@app.command("/users")
async def create_user(name: QueryParam[str]):
return div(f"Created: {name}")
@app.detached_command("/import")
async def import_data():
# Runs in background, returns 204 immediately
await long_running_import()
Available decorators:
@app.query(path)- Read operations (GET)@app.command(path)- Write operations (POST)@app.detached_command(path)- Fire-and-forget operations (POST, background execution)
Alternative: Route Constructor Approach ¶
If you prefer to define routes without decorators, pass them to the constructor or use app.add():
from stario import Stario
from stario.routes import Query, Command, DetachedCommand
app = Stario(
Query("/", index),
Query("/users/{user_id}", get_user),
Command("/users", create_user),
DetachedCommand("/import", import_data),
)
# Or add routes dynamically
app.add(Query("/search", search_results))
Constructor Parameters ¶
Stario(
*routes: BaseRoute,
middleware: Sequence[Middleware] | None = None,
compression_middleware: Middleware | None = BrotliMiddleware.as_middleware(),
exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
lifespan: Lifespan[AppType] | None = None,
debug: bool = False,
router_class: type[StarRouter] = StarRouter,
log_sinks: Sequence[Any] | None = None,
)
- routes: Variable number of route objects to register
- middleware: List of middleware to apply (see Middlewares)
- compression_middleware: Response compression (Brotli by default)
- exception_handlers: Map exception types or status codes to handler functions
- lifespan: Context manager for startup/shutdown tasks
- debug: Enable debug mode (affects error responses and logging)
- router_class: Router class to use (default:
StarRouter) - log_sinks: Custom logging sinks (auto-selected by default)
Application Methods ¶
Adding routes dynamically:
app.add(Query("/new-route", handler))
Mounting sub-applications or routers:
app.mount("/api", api_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
Host-based routing:
app.host("api.example.com", api_app, name="api")
Adding middleware after initialization:
app.add_middleware(CORSMiddleware, allow_origins=["*"])
Adding exception handlers:
app.add_exception_handler(404, custom_404_handler)
app.add_exception_handler(ValueError, handle_value_error)
State and caching:
# Access application state
app.state.db_pool = create_pool()
# Internal cache for singleton dependencies
app.state.cache # Automatically initialized
StarRouter ¶
StarRouter is Stario's router class, designed to organize routes in modular applications. It's particularly useful in multi-module projects where you want to group related routes together.
from stario.routing import StarRouter
from stario.routes import Query, Command
# In api/users.py
users_router = StarRouter(
Query("/", list_users),
Query("/{user_id}", get_user),
Command("/", create_user),
)
# In main app
app = Stario()
app.mount("/users", users_router)
Router Parameters ¶
StarRouter(
*routes: BaseRoute,
redirect_slashes: bool = True,
default: ASGIApp | None = None,
lifespan: Lifespan[Any] | None = None,
middleware: Sequence[Middleware] | None = None,
)
Multi-Module Pattern (Recommended) ¶
For larger applications, organize routes by feature or domain:
myapp/
├── app/
│ ├── main.py # Main application
│ ├── users/
│ │ ├── __init__.py
│ │ └── router.py # Users router
│ ├── posts/
│ │ ├── __init__.py
│ │ └── router.py # Posts router
│ └── api/
│ ├── __init__.py
│ └── router.py # API router
users/router.py:
from stario.routing import StarRouter
from stario.routes import Query, Command
from .handlers import list_users, get_user, create_user, update_user
router = StarRouter(
Query("/", list_users, name="list_users"),
Query("/{user_id}", get_user, name="get_user"),
Command("/", create_user, name="create_user"),
Command("/{user_id}", update_user, name="update_user"),
)
main.py:
from stario import Stario
from .users.router import router as users_router
from .posts.router import router as posts_router
from .api.router import router as api_router
app = Stario()
app.mount("/users", users_router)
app.mount("/posts", posts_router)
app.mount("/api", api_router)
Using Route Constructors ¶
Instead of decorators, you can pass routes to the constructor. For the complete syntax of each route type, see Endpoints.
from stario import Stario
from stario.routes import Query, Command, DetachedCommand
app = Stario(
Query("/", index, name="home"),
Query("/users/{user_id}", get_user, name="user_detail"),
Command("/users", create_user, name="create_user"),
DetachedCommand("/import", import_data),
)
Or add them dynamically:
app.add(Query("/search", search_results))
app.add(Command("/posts", create_post))
Path Parameters ¶
Use curly braces to define dynamic path segments:
Query("/users/{user_id}", get_user)
Query("/posts/{post_id}/comments/{comment_id}", get_comment)
Extract them in your handler using the PathParam dependency:
from stario.requests import PathParam
from stario.html import div
async def get_user(user_id: PathParam[int]):
# Type annotation determines conversion: int → 123 (not "123")
return div(f"User ID: {user_id}")
Type Conversion ¶
Stario supports two approaches to specify path parameter types:
Approach 1: Type annotation (recommended)
# Type comes from the PathParam annotation
async def handler(post_id: PathParam[int]):
# post_id is automatically converted to int
pass
Approach 2: Starlette converters (explicit)
# Type specified in path using Starlette syntax
Query("/posts/{post_id:int}", handler)
# Supported converters:
Query("/items/{item_id:int}", handler) # Integer
Query("/files/{filename:path}", handler) # Path (includes slashes)
Query("/tags/{tag:str}", handler) # String (default)
Both approaches are equivalent. Use whichever is more readable for your use case.
Mounting ¶
Mounting Routers ¶
Mount routers at specific path prefixes:
app.mount("/users", users_router)
app.mount("/admin", admin_router)
All routes in the mounted router will be prefixed with the mount path:
users_routerwith route"/"becomes/users/users_routerwith route"/{user_id}"becomes/users/{user_id}
Static Files ¶
Use Starlette's StaticFiles to serve static assets:
from starlette.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/media", StaticFiles(directory="uploads"), name="media")
Then reference them in your HTML:
from stario.html import link, script
link({"rel": "stylesheet", "href": "/static/css/styles.css"})
script({"src": "/static/js/app.js"})
Or use url_path_for() for type-safe URLs:
app.url_path_for("static", path="css/styles.css")
# Returns: "/static/css/styles.css"
Mounting ASGI Apps ¶
Mount any ASGI application:
from some_framework import create_app
legacy_app = create_app()
app.mount("/legacy", legacy_app)
URL Generation ¶
Stario borrows url_path_for() from Starlette, enabling type-safe URL generation from route names.
Basic Usage ¶
# In your routes
Query("/", index, name="home")
Query("/users/{user_id}", get_user, name="user_detail")
# Generate URLs
app.url_path_for("home") # "/"
app.url_path_for("user_detail", user_id=123) # "/users/123"
In Templates ¶
from stario.requests import Request
async def navigation(request: Request):
return nav(
a({"href": request.app.url_path_for("home")}, "Home"),
a({"href": request.app.url_path_for("user_detail", user_id=current_user_id)}, "Profile"),
)
In Datastar Actions ¶
The Datastar dependency automatically uses url_path_for() when you pass a route name (without leading /):
from stario.datastar import Datastar
async def my_button(ds: Datastar):
return button(
ds.on("click", ds.get("user_detail", user_id=123)),
"Load User",
)
If the URI starts with /, it's treated as an absolute path:
ds.get("/api/users") # Absolute path
ds.get("user_detail") # Route name, resolved via url_path_for()
Mounted Routes ¶
For mounted routers with names:
app.mount("/admin", admin_router, name="admin")
# Access routes in the mounted router using colon syntax
app.url_path_for("admin:users") # "/admin/users"
app.url_path_for("admin:user_detail", user_id=123) # "/admin/users/123"
This namespacing prevents conflicts when multiple routers define routes with the same name. For example, both admin_router and public_router could have a route named "users", accessed as "admin:users" and "public:users" respectively.
Route Names ¶
Route names enable URL generation and are automatically derived from the handler function name if not provided:
# Name automatically set to "get_user"
Query("/users/{user_id}", get_user)
# Explicit name
Query("/users/{user_id}", get_user, name="user_detail")
Best practices:
- Use explicit names for important routes you'll reference
- Use snake_case for consistency
- Make names descriptive:
user_detail,post_list,comment_create - Avoid generic names like
index,detail,listin mounted routers
Next Steps ¶
- Learn about Middlewares that wrap your routes
- Understand Endpoints handler syntax
- Explore Dependencies for request data extraction
