The little ASGI library that shines.
Starlette provides a flexible and powerful routing system for mapping URLs to endpoint functions, supporting HTTP routes, WebSocket routes, sub-application mounting, and host-based routing.
from starlette.routing import Router, BaseRoute
from starlette.middleware import Middleware
from starlette.types import ASGIApp, Lifespan
from typing import Sequence, Callable, Collection, Any
from starlette.requests import Request
from starlette.responses import Response
from starlette.websockets import WebSocket
class Router:
"""
Router handles request routing and dispatch.
The Router class manages:
- Route matching and parameter extraction
- URL generation from named routes
- Middleware application
- Lifespan event handling
"""
def __init__(
self,
routes: Sequence[BaseRoute] | None = None,
redirect_slashes: bool = True,
default: ASGIApp | None = None,
on_startup: Sequence[Callable] | None = None,
on_shutdown: Sequence[Callable] | None = None,
lifespan: Lifespan | None = None,
middleware: Sequence[Middleware] | None = None,
) -> None:
"""
Initialize router.
Args:
routes: List of route definitions
redirect_slashes: Automatically redirect /path/ to /path
default: Default ASGI app when no route matches
on_startup: Startup event handlers (deprecated)
on_shutdown: Shutdown event handlers (deprecated)
lifespan: Lifespan context manager
middleware: Middleware stack for this router
"""
@property
def routes(self) -> list[BaseRoute]:
"""List of registered routes."""
def url_path_for(self, name: str, /, **path_params) -> URLPath:
"""Generate URL path for named route."""
def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
"""Mount ASGI application at path."""
def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
"""Add host-based routing."""
def add_route(
self,
path: str,
endpoint: Callable[[Request], Awaitable[Response] | Response],
methods: Collection[str] | None = None,
name: str | None = None,
include_in_schema: bool = True,
) -> None:
"""Add HTTP route."""
def add_websocket_route(
self,
path: str,
endpoint: Callable[[WebSocket], Awaitable[None]],
name: str | None = None,
) -> None:
"""Add WebSocket route."""
def add_event_handler(self, event_type: str, func: Callable[[], Any]) -> None:
"""Add event handler (deprecated)."""
async def startup(self) -> None:
"""Execute startup handlers."""
async def shutdown(self) -> None:
"""Execute shutdown handlers."""
async def lifespan(self, scope, receive, send) -> None:
"""Handle ASGI lifespan protocol."""from starlette.routing import Route, Match
from starlette.middleware import Middleware
from starlette.types import Scope, Receive, Send
from starlette.datastructures import URLPath
from typing import Any, Callable, Collection, Sequence
class Route:
"""
HTTP route definition.
Maps URL patterns to endpoint functions for HTTP requests.
"""
def __init__(
self,
path: str,
endpoint: Callable[..., Any],
*,
methods: Collection[str] | None = None,
name: str | None = None,
include_in_schema: bool = True,
middleware: Sequence[Middleware] | None = None,
) -> None:
"""
Initialize HTTP route.
Args:
path: URL pattern with optional parameters
endpoint: Function or class to handle requests
methods: Allowed HTTP methods (default: ["GET"])
name: Route name for URL generation
include_in_schema: Include in OpenAPI schema
middleware: Route-specific middleware
"""
@property
def path(self) -> str:
"""Route path pattern."""
@property
def endpoint(self) -> Callable:
"""Route endpoint function."""
@property
def methods(self) -> set[str]:
"""Allowed HTTP methods."""
@property
def name(self) -> str | None:
"""Route name."""
def matches(self, scope: Scope) -> tuple[Match, Scope]:
"""Check if route matches request scope."""
def url_path_for(self, name: str, /, **path_params: Any) -> URLPath:
"""Generate URL path for this route."""
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Handle matched request."""from starlette.routing import WebSocketRoute
class WebSocketRoute:
"""
WebSocket route definition.
Maps URL patterns to WebSocket endpoint functions.
"""
def __init__(
self,
path: str,
endpoint: Callable,
name: str | None = None,
middleware: Sequence[Middleware] | None = None,
) -> None:
"""
Initialize WebSocket route.
Args:
path: URL pattern with optional parameters
endpoint: Function or class to handle WebSocket
name: Route name for URL generation
middleware: Route-specific middleware
"""
@property
def path(self) -> str:
"""Route path pattern."""
@property
def endpoint(self) -> Callable:
"""WebSocket endpoint function."""
@property
def name(self) -> str | None:
"""Route name."""
def matches(self, scope) -> tuple[Match, dict]:
"""Check if route matches WebSocket scope."""
def url_path_for(self, name: str, /, **path_params) -> URLPath:
"""Generate URL path for this route."""
async def handle(self, scope, receive, send) -> None:
"""Handle matched WebSocket connection."""from starlette.routing import Mount
class Mount:
"""
Mount point for sub-applications.
Mounts an ASGI application at a path prefix.
"""
def __init__(
self,
path: str,
app: ASGIApp | None = None,
routes: Sequence[BaseRoute] | None = None,
name: str | None = None,
middleware: Sequence[Middleware] | None = None,
) -> None:
"""
Initialize mount point.
Args:
path: Mount path prefix
app: ASGI application to mount
routes: Routes to mount (alternative to app)
name: Mount name for URL generation
middleware: Mount-specific middleware
"""
@property
def path(self) -> str:
"""Mount path prefix."""
@property
def app(self) -> ASGIApp:
"""Mounted ASGI application."""
@property
def name(self) -> str | None:
"""Mount name."""
def matches(self, scope) -> tuple[Match, dict]:
"""Check if mount matches request scope."""
def url_path_for(self, name: str, /, **path_params) -> URLPath:
"""Generate URL path within mount."""
async def handle(self, scope, receive, send) -> None:
"""Handle request to mounted application."""from starlette.routing import Host
class Host:
"""
Host-based routing.
Routes requests based on the Host header.
"""
def __init__(
self,
host: str,
app: ASGIApp,
name: str | None = None,
) -> None:
"""
Initialize host route.
Args:
host: Host pattern (supports wildcards)
app: ASGI application for this host
name: Host route name
"""
@property
def host(self) -> str:
"""Host pattern."""
@property
def app(self) -> ASGIApp:
"""Application for this host."""
@property
def name(self) -> str | None:
"""Host route name."""
def matches(self, scope) -> tuple[Match, dict]:
"""Check if host matches request."""
def url_path_for(self, name: str, /, **path_params) -> URLPath:
"""Generate URL path for host route."""
async def handle(self, scope, receive, send) -> None:
"""Handle request for this host."""from starlette.applications import Starlette
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route
# Simple function endpoints
async def homepage(request):
return PlainTextResponse("Hello, world!")
async def about(request):
return JSONResponse({"page": "about"})
# Route definitions
routes = [
Route("/", homepage, name="home"),
Route("/about", about, name="about"),
]
app = Starlette(routes=routes)from starlette.responses import JSONResponse
from starlette.routing import Route
# Multiple HTTP methods
async def user_handler(request):
if request.method == "GET":
return JSONResponse({"user": "data"})
elif request.method == "POST":
data = await request.json()
return JSONResponse({"created": data})
elif request.method == "DELETE":
return JSONResponse({"deleted": True})
routes = [
Route("/users", user_handler, methods=["GET", "POST", "DELETE"]),
]
# Method-specific routes
async def get_users(request):
return JSONResponse({"users": []})
async def create_user(request):
data = await request.json()
return JSONResponse({"created": data}, status_code=201)
routes = [
Route("/users", get_users, methods=["GET"]),
Route("/users", create_user, methods=["POST"]),
]from starlette.routing import Route
from starlette.responses import JSONResponse
# String parameters (default)
async def user_profile(request):
user_id = request.path_params["user_id"] # str
return JSONResponse({"user_id": user_id})
# Integer parameters
async def post_detail(request):
post_id = request.path_params["post_id"] # int
return JSONResponse({"post_id": post_id})
# Float parameters
async def coordinates(request):
lat = request.path_params["lat"] # float
lng = request.path_params["lng"] # float
return JSONResponse({"lat": lat, "lng": lng})
# UUID parameters
async def resource_by_uuid(request):
resource_id = request.path_params["resource_id"] # UUID
return JSONResponse({"id": str(resource_id)})
# Path parameters (matches any path including /)
async def file_handler(request):
file_path = request.path_params["file_path"] # str (can contain /)
return JSONResponse({"path": file_path})
routes = [
Route("/users/{user_id}", user_profile), # String
Route("/posts/{post_id:int}", post_detail), # Integer
Route("/map/{lat:float}/{lng:float}", coordinates), # Float
Route("/resources/{resource_id:uuid}", resource_by_uuid), # UUID
Route("/files/{file_path:path}", file_handler), # Path
]# Multiple parameters
async def user_post(request):
user_id = request.path_params["user_id"]
post_id = request.path_params["post_id"]
return JSONResponse({
"user_id": int(user_id),
"post_id": int(post_id)
})
# Optional parameters with query strings
async def search_posts(request):
user_id = request.path_params.get("user_id")
query = request.query_params.get("q", "")
return JSONResponse({
"user_id": int(user_id) if user_id else None,
"query": query
})
routes = [
Route("/users/{user_id:int}/posts/{post_id:int}", user_post),
Route("/users/{user_id:int}/posts", search_posts),
Route("/posts", search_posts), # user_id will be None
]from starlette.routing import WebSocketRoute
from starlette.websockets import WebSocket
# Simple WebSocket endpoint
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.send_text("Hello WebSocket!")
await websocket.close()
# WebSocket with path parameters
async def user_websocket(websocket: WebSocket):
user_id = websocket.path_params["user_id"]
await websocket.accept()
await websocket.send_json({
"type": "connected",
"user_id": int(user_id)
})
try:
while True:
data = await websocket.receive_json()
# Echo data back
await websocket.send_json({
"type": "echo",
"data": data,
"user_id": int(user_id)
})
except WebSocketDisconnect:
print(f"User {user_id} disconnected")
routes = [
WebSocketRoute("/ws", websocket_endpoint, name="websocket"),
WebSocketRoute("/ws/user/{user_id:int}", user_websocket, name="user_ws"),
]from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.staticfiles import StaticFiles
# API sub-application
async def api_root(request):
return JSONResponse({"api": "v1"})
async def api_users(request):
return JSONResponse({"users": []})
api_routes = [
Route("/", api_root),
Route("/users", api_users),
]
api_app = Starlette(routes=api_routes)
# Admin sub-application
async def admin_dashboard(request):
return JSONResponse({"page": "dashboard"})
admin_routes = [
Route("/", admin_dashboard),
]
admin_app = Starlette(routes=admin_routes)
# Main application
routes = [
Route("/", homepage),
Mount("/api/v1", api_app, name="api"),
Mount("/admin", admin_app, name="admin"),
Mount("/static", StaticFiles(directory="static"), name="static"),
]
app = Starlette(routes=routes)
# URLs will be:
# / -> homepage
# /api/v1/ -> api_root
# /api/v1/users -> api_users
# /admin/ -> admin_dashboard
# /static/* -> static filesfrom starlette.routing import Host
# Different apps for different hosts
main_app = Starlette(routes=[
Route("/", main_homepage),
])
api_app = Starlette(routes=[
Route("/", api_root),
Route("/users", api_users),
])
admin_app = Starlette(routes=[
Route("/", admin_dashboard),
])
# Host-based routing
routes = [
Host("api.example.com", api_app, name="api_host"),
Host("admin.example.com", admin_app, name="admin_host"),
Host("{subdomain}.example.com", main_app, name="subdomain_host"),
Host("example.com", main_app, name="main_host"),
]
app = Starlette(routes=routes)
# Requests route based on Host header:
# Host: api.example.com -> api_app
# Host: admin.example.com -> admin_app
# Host: blog.example.com -> main_app (subdomain match)
# Host: example.com -> main_appfrom starlette.routing import Route
from starlette.datastructures import URLPath
# Named routes for URL generation
routes = [
Route("/", homepage, name="home"),
Route("/users/{user_id:int}", user_profile, name="user"),
Route("/users/{user_id:int}/posts/{post_id:int}", post_detail, name="post"),
]
app = Starlette(routes=routes)
# Generate URLs
home_url = app.url_path_for("home")
# URLPath("/")
user_url = app.url_path_for("user", user_id=123)
# URLPath("/users/123")
post_url = app.url_path_for("post", user_id=123, post_id=456)
# URLPath("/users/123/posts/456")
# In request handlers
async def some_view(request):
# Generate absolute URLs
home_url = request.url_for("home")
user_url = request.url_for("user", user_id=123)
return JSONResponse({
"home": str(home_url), # "http://example.com/"
"user": str(user_url), # "http://example.com/users/123"
})
# URL generation with mounts
api_routes = [
Route("/users/{user_id:int}", api_user_detail, name="user_detail"),
]
routes = [
Mount("/api/v1", Starlette(routes=api_routes), name="api"),
]
app = Starlette(routes=routes)
# Generate mounted URLs
api_user_url = app.url_path_for("api:user_detail", user_id=123)
# URLPath("/api/v1/users/123")from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.routing import Route
# Route-specific middleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
class AuthRequiredMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if not request.headers.get("authorization"):
return JSONResponse(
{"error": "Authentication required"},
status_code=401
)
return await call_next(request)
# Apply middleware to specific routes
routes = [
Route("/", homepage), # No middleware
Route("/api/data", api_data, middleware=[
Middleware(TimingMiddleware),
]),
Route("/admin/users", admin_users, middleware=[
Middleware(AuthRequiredMiddleware),
Middleware(TimingMiddleware),
]),
]from starlette.routing import Match
# Route matching enum
class Match(Enum):
NONE = 0 # No match
PARTIAL = 1 # Partial match (for mounts)
FULL = 2 # Full match
# Custom route matching
def custom_matches(route, scope):
match, params = route.matches(scope)
if match == Match.FULL:
# Route fully matches
return params
elif match == Match.PARTIAL:
# Partial match (mount point)
return params
else:
# No match
return Nonefrom starlette.routing import request_response, websocket_session
# Convert function to ASGI application
def sync_endpoint(request):
# Sync function automatically wrapped
return JSONResponse({"sync": True})
async def async_endpoint(request):
# Async function used directly
return JSONResponse({"async": True})
# Convert WebSocket function
@websocket_session
async def websocket_endpoint(websocket):
await websocket.accept()
await websocket.send_text("Hello!")
await websocket.close()
routes = [
Route("/sync", sync_endpoint),
Route("/async", async_endpoint),
WebSocketRoute("/ws", websocket_endpoint),
]from starlette.routing import NoMatchFound
# Exception raised when URL generation fails
try:
url = app.url_path_for("nonexistent_route")
except NoMatchFound:
# Handle missing route
url = app.url_path_for("home") # FallbackThe routing system provides flexible URL mapping with support for path parameters, multiple HTTP methods, WebSocket connections, sub-application mounting, and host-based routing, making it suitable for complex web applications.
Install with Tessl CLI
npx tessl i tessl/pypi-starlette