Skip to content

Routing

Learn how to define and organize routes in your Wiverno application.

Overview

Wiverno provides flexible routing through decorators and explicit route lists. Routes map URL patterns to view functions or classes.

Basic Routing

The recommended way to define routes is using decorators:

from wiverno.main import Wiverno

app = Wiverno()

@app.route("/")
def index(request):
    """Homepage view."""
    return "Welcome!"

@app.get("/users")
def users_list(request):
    """List all users - GET only."""
    return "User list"

@app.post("/users")
def create_user(request):
    """Create user - POST only."""
    return 201, "User created"

HTTP Methods

Method-Specific Decorators

Wiverno provides decorators for common HTTP methods:

@app.get("/items")
def list_items(request):
    """Handle GET requests."""
    return "Items list"

@app.post("/items")
def create_item(request):
    """Handle POST requests."""
    return 201, "Item created"

@app.put("/items")
def update_item(request):
    """Handle PUT requests."""
    item_id = request.query_params.get("id")
    return f"Updated item {item_id}"

@app.delete("/items")
def delete_item(request):
    """Handle DELETE requests."""
    item_id = request.query_params.get("id")
    return 204, ""

@app.patch("/items")
def patch_item(request):
    """Handle PATCH requests."""
    return "Item patched"

Custom Method Lists

Specify allowed methods explicitly:

@app.route("/api/data", methods=["GET", "POST", "PUT"])
def handle_data(request):
    """Handle multiple HTTP methods."""
    if request.method == "GET":
        return "Data retrieved"
    elif request.method == "POST":
        return 201, "Data created"
    elif request.method == "PUT":
        return "Data updated"

Path Parameters

Wiverno supports dynamic path parameters with FastAPI-style syntax and automatic type conversion.

Basic Path Parameters

Define path parameters using curly braces:

@app.get("/users/{id}")
def user_detail(request):
    """Get user by ID from path parameter."""
    user_id = request.path_params["id"]  # String by default
    return f"<h1>User: {user_id}</h1>"

# Usage: /users/123
# request.path_params = {"id": "123"}

Typed Path Parameters

Add type hints for automatic conversion:

@app.get("/users/{id:int}")
def get_user(request):
    """Get user with integer ID."""
    user_id = request.path_params["id"]  # Already converted to int
    return f"<h1>User ID: {user_id}</h1>"

@app.get("/products/{price:float}")
def get_product_by_price(request):
    """Get products by price."""
    price = request.path_params["price"]  # Already converted to float
    return f"<p>Price: ${price:.2f}</p>"

# Usage: /users/123 -> user_id = 123 (int)
# Usage: /products/19.99 -> price = 19.99 (float)

Supported Parameter Types

  • {name} - String parameter (default, matches [^/]+)
  • {name:str} - Explicit string parameter
  • {name:int} - Integer parameter (matches [0-9]+)
  • {name:float} - Float parameter (matches [0-9]+\.?[0-9]*)
  • {name:path} - Path parameter (matches .+, includes slashes)

Multiple Path Parameters

Combine multiple parameters in one route:

@app.get("/posts/{slug}/comments/{comment_id:int}")
def get_comment(request):
    """Get comment from specific post."""
    slug = request.path_params["slug"]           # str
    comment_id = request.path_params["comment_id"]  # int
    return f"<p>Post: {slug}, Comment: {comment_id}</p>"

# Usage: /posts/hello-world/comments/42
# request.path_params = {"slug": "hello-world", "comment_id": 42}

Path Parameter for File Paths

Use :path type to capture paths with slashes:

@app.get("/files/{filepath:path}")
def serve_file(request):
    """Serve file from nested path."""
    filepath = request.path_params["filepath"]  # Can contain slashes
    return f"<p>File: {filepath}</p>"

# Usage: /files/docs/guide/intro.md
# request.path_params = {"filepath": "docs/guide/intro.md"}

Using Query Parameters

Query parameters work alongside path parameters:

@app.get("/users/{id:int}/posts")
def user_posts(request):
    """Get user's posts with pagination."""
    user_id = request.path_params["id"]          # From path
    limit = request.query_params.get("limit", "10")   # From query string
    offset = request.query_params.get("offset", "0")
    return f"<p>User {user_id}: Posts (limit={limit}, offset={offset})</p>"

# Usage: /users/42/posts?limit=20&offset=10

Multiple Query Parameter Values

Use getlist() for repeated query parameters:

@app.get("/search")
def search(request):
    """Search with multiple tags."""
    tags = request.query_params.getlist("tag")  # Get all tag values
    query = request.query_params.get("q", "")
    return f"<p>Search: {query}, Tags: {', '.join(tags)}</p>"

# Usage: /search?q=python&tag=web&tag=framework&tag=wsgi
# request.query_params.getlist("tag") = ["web", "framework", "wsgi"]

Using Router Class

For modular applications, use the Router class:

from wiverno.core.routing.router import Router
from wiverno import Wiverno

# Create a router for API endpoints
api_router = Router()

@api_router.get("/users")
def api_users(request):
    """API: List users."""
    return '{"users": []}'

@api_router.post("/users")
def api_create_user(request):
    """API: Create user."""
    return 201, '{"id": 1}'

@api_router.get("/users/{id:int}")
def api_user_detail(request):
    """API: Get user details."""
    user_id = request.path_params["id"]  # int from path
    return f'{{"id": {user_id}}}'

# Create app and include router
app = Wiverno()
app.include_router(api_router, prefix="/api/v1")

# Routes become:
# GET  /api/v1/users
# POST /api/v1/users
# GET  /api/v1/users/{id:int}

Router with Prefix

Organize routes by feature:

# Blog routes
blog_router = Router()

@blog_router.get("/")
def blog_index(request):
    """Blog homepage."""
    return "<h1>Blog posts</h1>"

@blog_router.get("/{slug}")
def blog_post(request):
    """Single blog post."""
    slug = request.path_params["slug"]
    return f"<h1>Post: {slug}</h1>"

@blog_router.get("/{slug}/comments/{comment_id:int}")
def blog_comment(request):
    """Single comment on a blog post."""
    slug = request.path_params["slug"]
    comment_id = request.path_params["comment_id"]
    return f"<p>Comment {comment_id} on {slug}</p>"

# Admin routes
admin_router = Router()

@admin_router.get("/dashboard")
def admin_dashboard(request):
    """Admin dashboard."""
    return "<h1>Admin Dashboard</h1>"

@admin_router.get("/users/{id:int}")
def admin_user(request):
    """Admin user details."""
    user_id = request.path_params["id"]
    return f"<h1>Admin: User {user_id}</h1>"

# Combine in app
app = Wiverno()
app.include_router(blog_router, prefix="/blog")
app.include_router(admin_router, prefix="/admin")

# Routes:
# /blog/ -> blog_index
# /blog/my-first-post -> blog_post
# /blog/my-first-post/comments/5 -> blog_comment
# /admin/dashboard -> admin_dashboard
# /admin/users/42 -> admin_user

Path Normalization

Wiverno automatically normalizes paths:

# These are all equivalent:
@app.route("/users")
@app.route("/users/")
@app.route("users")
@app.route("users/")

# All match: /users (trailing slash removed)

Root path is special:

@app.route("/")  # Homepage - keeps the single slash

Route Priority

Routes are matched with intelligent priority:

Static Routes Take Precedence

Static routes (without parameters) are checked first with O(1) lookup:

@app.get("/users/admin")  # Static route - checked first
def admin_users(request):
    return "<h1>Admin users</h1>"

@app.get("/users/{id:int}")  # Dynamic route - checked after static
def user_detail(request):
    user_id = request.path_params["id"]
    return f"<h1>User {user_id}</h1>"

# /users/admin -> admin_users (static route wins)
# /users/123 -> user_detail (dynamic route matches)

Dynamic Route Specificity

Dynamic routes are sorted by specificity (more segments = higher priority):

@app.get("/posts/{slug}/comments/{id:int}")  # 4 segments - higher priority
def post_comment(request):
    slug = request.path_params["slug"]
    comment_id = request.path_params["id"]
    return f"<p>Comment {comment_id} on {slug}</p>"

@app.get("/posts/{slug}")  # 2 segments - lower priority
def post_detail(request):
    slug = request.path_params["slug"]
    return f"<h1>Post: {slug}</h1>"

# /posts/hello-world/comments/5 -> post_comment (more specific)
# /posts/hello-world -> post_detail

Route Conflict Detection

Registering the same path+method twice raises an error:

@app.get("/users")
def users1(request):
    return "Users v1"

@app.get("/users")  # ❌ Raises RouteConflictError
def users2(request):
    return "Users v2"

Class-Based Views with Routes

Use class-based views for better organization:

from wiverno.views.base_views import BaseView
from wiverno import Wiverno

class UserView(BaseView):
    """Handle user operations."""

    def get(self, request):
        """Get user by ID."""
        user_id = request.path_params["id"]
        return f"<h1>User {user_id}</h1>"

    def put(self, request):
        """Update user."""
        user_id = request.path_params["id"]
        return f"<p>User {user_id} updated</p>"

    def delete(self, request):
        """Delete user."""
        return 204, ""

class UserListView(BaseView):
    """Handle user list operations."""

    def get(self, request):
        """List all users."""
        return "<ul><li>User 1</li><li>User 2</li></ul>"

    def post(self, request):
        """Create new user."""
        return 201, "<p>User created</p>"

# Register class-based views
app = Wiverno()
app.route("/users")(UserListView())
app.route("/users/{id:int}")(UserView())

Error Handling

404 Not Found

Automatically handled when no route matches:

# Request to /nonexistent returns 404

405 Method Not Allowed

When route exists but method is not allowed:

@app.get("/users")
def users(request):
    return "Users"

# POST /users returns 405

Custom Error Handlers

Provide custom error pages:

class Custom404:
    def __call__(self, request):
        """Custom 404 handler."""
        return 404, "<h1>Page Not Found</h1>"

class Custom405:
    def __call__(self, request):
        """Custom 405 handler."""
        method = request.method
        return 405, f"<h1>Method {method} Not Allowed</h1>"

class Custom500:
    def __call__(self, request, error_traceback=None):
        """Custom 500 handler."""
        return 500, "<h1>Server Error</h1>"

app = Wiverno(
    page_404=Custom404(),
    page_405=Custom405(),
    page_500=Custom500()
)

Best Practices

1. Organize by Feature

# users.py
users_router = Router()

@users_router.get("/")
def list_users(request):
    pass

@users_router.post("/")
def create_user(request):
    pass

# posts.py
posts_router = Router()

@posts_router.get("/")
def list_posts(request):
    pass

# main.py
app = Wiverno()
app.include_router(users_router, prefix="/users")
app.include_router(posts_router, prefix="/posts")

2. Use Descriptive Names

# Good
@app.get("/users/{user_id:int}/posts/{post_id:int}")
def get_user_post(request):
    user_id = request.path_params["user_id"]
    post_id = request.path_params["post_id"]
    return f"<h1>User {user_id}, Post {post_id}</h1>"

# Bad
@app.get("/users/{user_id:int}/posts/{post_id:int}")
def handler(request):
    pass

3. RESTful Design

# Resources: /users
@app.get("/users")               # List all users
@app.post("/users")              # Create user
@app.get("/users/{id:int}")      # Get single user
@app.put("/users/{id:int}")      # Update user
@app.delete("/users/{id:int}")   # Delete user

# Nested resources: /users/{user_id}/posts
@app.get("/users/{user_id:int}/posts")              # List user's posts
@app.post("/users/{user_id:int}/posts")             # Create post for user
@app.get("/users/{user_id:int}/posts/{post_id:int}")# Get specific post
@app.put("/users/{user_id:int}/posts/{post_id:int}")# Update post
@app.delete("/users/{user_id:int}/posts/{post_id:int}")# Delete post

4. Version Your API

api_v1 = Router()
# ... define v1 routes

api_v2 = Router()
# ... define v2 routes

app.include_router(api_v1, prefix="/api/v1")
app.include_router(api_v2, prefix="/api/v2")

Next Steps