Перейти к содержанию

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?

  1. Scalability - Manage hundreds of routes without chaos
  2. Clean Design - Separate concerns by feature/module
  3. Reduced Imports - Import entire route modules at once
  4. 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_accounts
  • POST /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: LoggingMiddlewareAuthMiddleware → 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_params dictionary.


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 → LoggingMiddlewareAuthMiddleware → 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

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: