Routing¶
Warning
The current page still doesn't have a translation for this language.
But you can help translating it: Contributing.
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