Routingยถ
Ravyn's routing system scales from a single route to hundreds of organized endpoints. Whether you're building a quick prototype or an enterprise application, the routing system handles it elegantly.
What You'll Learnยถ
- How to create routes with Gateway and WebSocketGateway
- Organizing routes with Include for scalable applications
- Using path parameters and custom converters
- Managing route priority and avoiding conflicts
- Adding middleware, permissions, and dependencies to routes
Quick Startยถ
Here's the simplest way to create routes:
from ravyn import Ravyn, get, post
app = Ravyn()
@app.get("/users")
def list_users() -> dict:
return {"users": ["Alice", "Bob"]}
@app.post("/users")
def create_user(name: str) -> dict:
return {"created": name}
That's it! Visit /users to see your route in action.
Gateway: The Route Wrapperยถ
A Gateway wraps a handler function and maps it to a URL path. It's more powerful than simple decorators because it allows you to organize routes separately from handlers.
Basic Gatewayยถ
from ravyn import Ravyn, Gateway, get
@get()
def welcome() -> dict:
return {"message": "Welcome!"}
app = Ravyn(
routes=[
Gateway("/", handler=welcome)
]
)
Gateway with Path Parametersยถ
from ravyn import Ravyn, Gateway, get
@get()
def get_user(user_id: int) -> dict:
return {"user_id": user_id, "name": "Alice"}
app = Ravyn(
routes=[
Gateway("/users/{user_id:int}", handler=get_user)
]
)
Tip
If you don't provide a path to Gateway, it defaults to /. The handler's decorator can also specify a path that gets appended to the Gateway path.
Automatic Gateway Wrappingยถ
You can pass handlers directly. Ravyn automatically wraps them in a Gateway:
from ravyn import Ravyn, get
@get("/users")
def list_users() -> dict:
return {"users": []}
# These are equivalent:
app1 = Ravyn(routes=[list_users]) # Auto-wrapped
app2 = Ravyn(routes=[Gateway("/users", handler=list_users)]) # Explicit
Reference: See all Gateway parameters.
WebSocketGateway: Real-Time Communicationยถ
For WebSocket connections, use WebSocketGateway. WebSocket handlers must be async.
from ravyn import Ravyn, WebSocket, WebSocketGateway, websocket
@websocket()
async def chat_socket(socket: WebSocket) -> None:
await socket.accept()
message = await socket.receive_json()
await socket.send_json({"echo": message})
await socket.close()
app = Ravyn(
routes=[
WebSocketGateway("/chat", handler=chat_socket)
]
)
Bidirectional Communicationยถ
Handle complex message loops and bidirectional data flow in your WebSocket gateways.
from ravyn import Ravyn, WebSocket, WebSocketGateway, websocket
from ravyn.websockets import WebSocketDisconnect
@websocket()
async def chat_hub(socket: WebSocket) -> None:
await socket.accept()
try:
while True:
# Receive JSON data from the client
data = await socket.receive_json()
# Process and respond
await socket.send_json({
"user": "System",
"msg": f"Echo: {data['msg']}"
})
except WebSocketDisconnect:
# Handle client disconnection gracefully
pass
app = Ravyn(routes=[WebSocketGateway("/chat", handler=chat_hub)])
Connection Lifecycleยถ
Manage the full lifecycle of a WebSocket connection, from acceptance to closure.
from ravyn import Ravyn, WebSocket, WebSocketGateway, websocket
@websocket()
async def lifecycle_ws(socket: WebSocket) -> None:
# 1. Accept the incoming connection
await socket.accept()
# 2. Main communication loop
for _ in range(5):
message = await socket.receive_text()
await socket.send_text(f"Processed: {message}")
# 3. Cleanly close the connection
await socket.close()
app = Ravyn(routes=[WebSocketGateway("/lifecycle", handler=lifecycle_ws)])
Reference: See all WebSocketGateway parameters.
Include: Organize Routes at Scaleยถ
Include is Ravyn's secret weapon for organizing large applications. It lets you split routes across multiple files and import them cleanly.
Warning
Include does NOT support path parameters. Don't use Include('/api/{id:int}', ...).
Why Use Include?ยถ
- Scalability - Manage hundreds of routes without chaos
- Clean Design - Separate concerns by feature/module
- Reduced Imports - Import entire route modules at once
- Fewer Bugs - Less manual route registration
Include with Namespaceยถ
The most common pattern. import routes from a module:
# accounts/urls.py
from ravyn import Gateway
from .controllers import list_accounts, create_account
route_patterns = [
Gateway("/", handler=list_accounts),
Gateway("/create", handler=create_account),
]
# app.py
from ravyn import Ravyn, Include
app = Ravyn(
routes=[
Include("/accounts", namespace="myapp.accounts.urls")
]
)
This creates:
GET /accounts/โ list_accountsPOST /accounts/createโ create_account
Tip
By default, Include looks for a route_patterns list in the namespace. You can change this with the pattern parameter.
Include with Routes Listยถ
Pass routes directly instead of using a namespace:
from ravyn import Ravyn, Include, Gateway
from myapp.accounts.controllers import list_accounts
app = Ravyn(
routes=[
Include("/accounts", routes=[
Gateway("/", handler=list_accounts)
])
]
)
Custom Pattern Nameยถ
Use a different variable name instead of route_patterns:
# accounts/urls.py
my_custom_routes = [ # Not 'route_patterns'
Gateway("/", handler=list_accounts),
]
# app.py
app = Ravyn(
routes=[
Include("/accounts", namespace="myapp.accounts.urls", pattern="my_custom_routes")
]
)
Reference: See all Include parameters.
Nested Routesยถ
Include supports nesting for complex applications:
Simple Nestingยถ
from ravyn import Ravyn, Include
app = Ravyn(
routes=[
Include("/api", routes=[
Include("/v1", namespace="myapp.api.v1.urls"),
Include("/v2", namespace="myapp.api.v2.urls"),
])
]
)
This creates:
/api/v1/...routes/api/v2/...routes
Complex Nesting with Featuresยถ
Each level can have its own middleware, permissions, dependencies, and exception handlers:
from ravyn import Ravyn, Include, Gateway
from lilya.middleware import DefineMiddleware
from myapp.middleware import LoggingMiddleware, AuthMiddleware
app = Ravyn(
routes=[
Include("/api",
middleware=[DefineMiddleware(LoggingMiddleware)],
routes=[
Include("/v1",
middleware=[DefineMiddleware(AuthMiddleware)],
namespace="myapp.api.v1.urls"
)
]
)
]
)
Middleware executes in order: LoggingMiddleware โ AuthMiddleware โ handler.
Route Groups with Shared Configurationยถ
Group related routes to share common configuration like middleware, permissions, and dependencies using Include.
from ravyn import Ravyn, Include, Gateway, get
from lilya.middleware import DefineMiddleware
# from myapp.middleware import LoggingMiddleware
@get("/users")
def list_users() -> list:
return []
# Grouping routes with shared middleware
user_routes = Include(
"/users",
routes=[Gateway("/", handler=list_users)],
# Apply middleware to the entire group
# middleware=[DefineMiddleware(LoggingMiddleware)]
)
app = Ravyn(routes=[user_routes])
Route Groups with Permissionsยถ
Share access control logic across multiple endpoints.
from ravyn import Ravyn, Include, Gateway, get
# from ravyn.permissions import IsAuthenticated, IsAdmin
@get("/settings")
def user_settings() -> dict:
return {}
# Grouping routes with shared permissions
admin_routes = Include(
"/admin",
routes=[Gateway("/settings", handler=user_settings)],
# All routes in this group require these permissions
# permissions=[IsAuthenticated, IsAdmin]
)
app = Ravyn(routes=[admin_routes])
Path Parametersยถ
Capture dynamic values from URLs using path parameters.
Basic Path Parametersยถ
from ravyn import Gateway, get
@get()
def get_customer(customer_id: str) -> dict:
return {"customer_id": customer_id}
Gateway("/customers/{customer_id}", handler=get_customer)
Type Convertersยถ
Ravyn supports these built-in converters:
| Converter | Python Type | Example |
|---|---|---|
str |
str (default) |
/users/{name} |
int |
int |
/users/{id:int} |
float |
float |
/prices/{amount:float} |
uuid |
uuid.UUID |
/items/{item_id:uuid} |
path |
str (with /) |
/files/{filepath:path} |
from ravyn import Gateway, get
@get()
def get_user(user_id: int) -> dict: # Receives int, not str
return {"user_id": user_id}
@get()
def get_file(filepath: str) -> dict: # Can include slashes
return {"filepath": filepath}
app = Ravyn(routes=[
Gateway("/users/{user_id:int}", handler=get_user),
Gateway("/files/{filepath:path}", handler=get_file),
])
Custom Convertersยถ
Create your own path converters:
import datetime
from lilya.routing.converters import Converter, register_converter
class DateTimeConverter(Converter):
regex = r"\d{4}-\d{2}-\d{2}"
def convert(self, value: str) -> datetime.datetime:
return datetime.datetime.strptime(value, "%Y-%m-%d")
def to_string(self, value: datetime.datetime) -> str:
return value.strftime("%Y-%m-%d")
# Register it
register_converter("datetime", DateTimeConverter)
# Use it
@get()
def sales_report(date: datetime.datetime) -> dict:
return {"date": date.isoformat()}
Gateway("/sales/{date:datetime}", handler=sales_report)
[!INFO] Path parameters are also available in
request.path_paramsdictionary.
Query Parametersยถ
Access URL query strings like ?search=python&limit=10 in your handlers.
Basic Query Parameter Accessยถ
Access query strings directly from the Request object.
from ravyn import Ravyn, Request, get
@get("/search")
def search(request: Request) -> dict:
query = request.query_params.get("q", "")
return {"query": query}
app = Ravyn(routes=[search])
Visit /search?q=python to see it work.
Optional Query Parameters with Defaultsยถ
Handle pagination and filters with default values for optional parameters.
from ravyn import Ravyn, Request, get
@get("/products")
def list_products(request: Request) -> dict:
limit = int(request.query_params.get("limit", 10))
offset = int(request.query_params.get("offset", 0))
return {
"limit": limit,
"offset": offset,
"products": []
}
app = Ravyn(routes=[list_products])
Multiple Query Parameters with Type Conversionยถ
Query parameters are strings by default. Convert them to the necessary Python types.
from typing import Optional
from ravyn import Ravyn, Request, get
@get("/filter")
def filter_items(request: Request) -> dict:
# Get optional category
category: Optional[str] = request.query_params.get("category")
# Simple boolean conversion
is_active_str = request.query_params.get("active", "true").lower()
active: bool = is_active_str == "true"
return {"category": category, "active": active}
app = Ravyn(routes=[filter_items])
Request Body Handlingยถ
Handle various incoming data types, from JSON payloads to form data.
JSON Request Body with Pydanticยถ
Use Pydantic models to automatically validate and parse incoming JSON data.
from pydantic import BaseModel
from ravyn import Ravyn, post
class UserCreate(BaseModel):
username: str
email: str
age: int
@post("/users")
def create_user(data: UserCreate) -> dict:
# Data is already validated and converted to the model
return {"status": "created", "user": data.model_dump()}
app = Ravyn(routes=[create_user])
Form Data Handlingยถ
Handle traditional HTML form submissions.
from ravyn import Ravyn, Request, post
@post("/submit")
async def submit_form(request: Request) -> dict:
# Form data is processed asynchronously
form_data = await request.form()
return {
"received": dict(form_data),
"username": form_data.get("username")
}
app = Ravyn(routes=[submit_form])
Route Priorityยถ
Routes are matched in the order they're defined. More specific routes must come first.
Correct Orderยถ
from ravyn import Ravyn, Gateway, get
@get()
def special_user() -> dict:
return {"type": "special"}
@get()
def get_user(user_id: int) -> dict:
return {"user_id": user_id}
# Correct - specific route first
app = Ravyn(routes=[
Gateway("/users/special", handler=special_user), # Matches first
Gateway("/users/{user_id:int}", handler=get_user), # Matches after
])
Incorrect Orderยถ
# Wrong - generic route first
app = Ravyn(routes=[
Gateway("/users/{user_id:int}", handler=get_user), # Matches everything!
Gateway("/users/special", handler=special_user), # Never reached
])
Warning
Ravyn does some automatic sorting (routes with only / path go last), but you should still order routes from most specific to least specific.
Adding Features to Routesยถ
All route objects (Gateway, WebSocketGateway, Include) support middleware, permissions, dependencies, and exception handlers.
Middlewareยถ
from ravyn import Ravyn, Include, Gateway, get
from lilya.middleware import DefineMiddleware
from myapp.middleware import LoggingMiddleware, AuthMiddleware
@get()
def handler() -> dict:
return {"message": "success"}
app = Ravyn(
routes=[
Include("/api",
middleware=[DefineMiddleware(LoggingMiddleware)],
routes=[
Gateway("/test",
handler=handler,
middleware=[DefineMiddleware(AuthMiddleware)]
)
]
)
]
)
Execution order: App middleware โ LoggingMiddleware โ AuthMiddleware โ handler.
Learn more in Middleware.
Exception Handlersยถ
from ravyn import Ravyn, Gateway, get
from ravyn.exceptions import NotAuthorized
def handle_not_authorized(request, exc):
return JSONResponse({"error": "Not authorized"}, status_code=401)
@get()
def protected() -> dict:
raise NotAuthorized("No access")
app = Ravyn(
routes=[
Gateway("/protected",
handler=protected,
exception_handlers={NotAuthorized: handle_not_authorized}
)
]
)
Learn more in Exception Handlers.
Dependenciesยถ
from ravyn import Ravyn, Gateway, Inject, Injects, get
def get_database():
return {"db": "connected"}
@get()
def users(db: dict = Injects()) -> dict:
return {"users": [], "db": db}
app = Ravyn(
routes=[
Gateway("/users",
handler=users,
dependencies={"db": Inject(get_database)}
)
]
)
Learn more in Dependencies.
Permissionsยถ
from ravyn import Ravyn, Gateway, get
from ravyn.permissions import IsAuthenticated
@get(permissions=[IsAuthenticated])
def protected() -> dict:
return {"message": "You're authenticated!"}
app = Ravyn(
routes=[
Gateway("/protected", handler=protected)
]
)
Learn more in Permissions.
Common Pitfalls & Fixesยถ
Pitfall 1: Include Without Path Causes Route Conflictsยถ
Problem: Multiple Include statements without paths override each other.
# Wrong - both default to '/'
app = Ravyn(routes=[
Include(namespace="myapp.urls"), # Path defaults to '/'
Include(namespace="accounts.urls"), # Also defaults to '/'
# Only one will be registered!
])
Solution: Always specify paths for Include:
# Correct
app = Ravyn(routes=[
Include("/api", namespace="myapp.urls"),
Include("/accounts", namespace="accounts.urls"),
])
Pitfall 2: Generic Routes Before Specific Routesยถ
Problem: Generic route matches everything, specific route never reached.
# Wrong
app = Ravyn(routes=[
Gateway("/users/{id:int}", handler=get_user), # Catches /users/me
Gateway("/users/me", handler=current_user), # Never reached!
])
Solution: Put specific routes first:
# Correct
app = Ravyn(routes=[
Gateway("/users/me", handler=current_user), # Checked first
Gateway("/users/{id:int}", handler=get_user), # Checked second
])
Pitfall 3: Using Path Parameters in Includeยถ
Problem: Include doesn't support path parameters.
# Wrong
app = Ravyn(routes=[
Include("/users/{user_id:int}", namespace="myapp.users.urls")
])
Solution: Use path parameters in Gateway, not Include:
# Correct
# myapp/users/urls.py
route_patterns = [
Gateway("/{user_id:int}/profile", handler=get_profile),
Gateway("/{user_id:int}/settings", handler=get_settings),
]
# app.py
app = Ravyn(routes=[
Include("/users", namespace="myapp.users.urls")
])
# Creates: /users/{user_id:int}/profile, /users/{user_id:int}/settings
Pitfall 4: Forgetting Async for WebSocketsยถ
Problem: WebSocket handler is not async.
# Wrong
@websocket()
def chat(socket: WebSocket): # Missing 'async'
await socket.accept() # SyntaxError!
Solution: WebSocket handlers must be async:
# Correct
@websocket()
async def chat(socket: WebSocket): # Added 'async'
await socket.accept()
Route Organization Patternsยถ
Pattern 1: Feature-Based (Recommended)ยถ
myapp/
โโโ app.py
โโโ urls.py
โโโ accounts/
โ โโโ controllers.py
โ โโโ urls.py
โโโ products/
โ โโโ controllers.py
โ โโโ urls.py
โโโ orders/
โโโ controllers.py
โโโ urls.py
Pattern 2: API Versioningยถ
myapp/
โโโ app.py
โโโ api/
โโโ v1/
โ โโโ urls.py
โ โโโ endpoints/
โโโ v2/
โโโ urls.py
โโโ endpoints/
# app.py
app = Ravyn(routes=[
Include("/api/v1", namespace="myapp.api.v1.urls"),
Include("/api/v2", namespace="myapp.api.v2.urls"),
])
Next Stepsยถ
Now that you understand routing, explore:
- Handlers - Different handler types and patterns
- Dependencies - Inject dependencies into routes
- Middleware - Add request/response processing
- Permissions - Secure your routes
- Application Levels - Understand the hierarchy