Skip to content

Routing System

Framefox’s routing system provides a way to define how your application responds to different HTTP requests. Built on top of FastAPI, it offers good flexibility while maintaining clean, readable syntax that makes route definition intuitive and maintainable.

The routing system is the backbone of your web application, determining which controller methods handle specific URL patterns and HTTP methods. Whether you’re building a simple website or a complex API, Framefox’s routing capabilities scale with your needs.

The @Route decorator is the primary way to define routes in Framefox. It transforms regular controller methods into HTTP endpoints, automatically handling request routing, parameter extraction, and response formatting.

from framefox.core.routing.decorator.route import Route
@Route(path="/users", name="user.index", methods=["GET"])
async def index(self):
return {"users": []}

This simple decorator call creates a complete HTTP endpoint that responds to GET requests at the /users path, with the unique identifier user.index for URL generation and reference.

Understanding each parameter helps you create more precise and maintainable routes:

  • path (str, required): The URL pattern that triggers this route

    • Static paths: /users, /about, /api/health
    • Dynamic paths: /users/{id}, /posts/{slug}/comments/{comment_id}
    • Wildcard paths: /files/{filepath:path} (captures remaining path segments)
  • name (str, required): A unique identifier for the route

    • Used for URL generation: generate_url("user.index")
    • Enables reverse routing and refactoring safety
    • Convention: {resource}.{action} (e.g., user.show, post.create)
  • methods (list, optional): HTTP methods this route accepts

    • Defaults to ["GET"] if not specified
    • Common values: ["GET"], ["POST"], ["PUT", "PATCH"], ["DELETE"]
    • Multiple methods: ["GET", "POST"] for form handling

Static routes handle fixed URL patterns without any dynamic parameters. They’re perfect for pages like home, about, contact, or any content that doesn’t require URL parameters.

@Route("/", "home.index", methods=["GET"])
async def home(self):
"""Homepage - most visited route."""
return self.render("home.html", {
"featured_posts": await self.post_service.get_featured(),
"site_stats": await self.analytics_service.get_public_stats()
})
@Route("/about", "about.page", methods=["GET"])
async def about(self):
"""About page with company information."""
return self.render("about.html", {
"team_members": await self.team_service.get_public_members(),
"company_history": await self.content_service.get_about_content()
})
@Route("/contact", "contact.page", methods=["GET"])
async def contact(self):
"""Contact page with form."""
return self.render("contact.html", {
"contact_form": self.create_form(ContactType, Contact()),
"office_locations": await self.location_service.get_offices()
})
# API health check endpoint
@Route("/health", "health.check", methods=["GET"])
async def health_check(self):
"""System health check for monitoring."""
return self.json({
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": "1.0.0",
"database": await self.db_service.check_connection(),
"redis": await self.cache_service.check_connection()
})

Services are automatically discovered and registered through Framefox’s comprehensive scanning system:

Discovery Process:

  1. Framework Core: All modules in framefox.core.* are scanned immediately
  2. Controllers: Classes in src/controller/ are always discovered during registration
  3. Background Scan: src/service/ and src/repository/ are scanned asynchronously
  4. Caching: Discovered services are cached for performance

Exclusions:

  • Entity directories (entity, entities, migration, migrations)
  • Test directories (test, tests, __pycache__)
  • Static assets and templates

The injection system can raise several specific exceptions:

# Service container errors
- ServiceNotFoundError: Service not registered and cannot be auto-registered
- ServiceInstantiationError: Failed to create service instance
- CircularDependencyError: Circular dependency detected
# Route-level errors
- RuntimeError: Generic injection failure with descriptive message

Parameterized routes capture dynamic segments from the URL and pass them as arguments to your controller methods. This enables you to build flexible, data-driven applications that can handle variable content based on URL parameters.

Single Parameter Routes:

@Route("/users/{id}", "user.show", methods=["GET"])
async def show_user(self, id: int):
"""Display a specific user by ID."""
try:
user = await self.user_service.get_by_id(id)
if not user:
return self.json({"error": "User not found"}, status=404)
return self.render("users/show.html", {
"user": user,
"edit_url": self.generate_url("user.edit", id=user.id),
"delete_url": self.generate_url("user.delete", id=user.id)
})
except ValueError:
# Invalid ID format
self.flash("error", "Invalid user ID")
return self.redirect(self.generate_url("user.index"))
@Route("/posts/{slug}", "post.show", methods=["GET"])
async def show_post(self, slug: str):
"""Display a post by URL slug."""
post = await self.post_service.get_by_slug(slug)
if not post:
return self.json({"error": "Post not found"}, status=404)
# Track view count
await self.analytics_service.track_post_view(post.id)
return self.render("posts/show.html", {
"post": post,
"related_posts": await self.post_service.get_related(post),
"comments": await self.comment_service.get_by_post(post.id)
})

Multiple Parameter Routes:

@Route("/users/{user_id}/posts/{post_id}", "user.post.show", methods=["GET"])
async def show_user_post(self, user_id: int, post_id: int):
"""Display a specific post by a specific user."""
# Verify user exists
user = await self.user_service.get_by_id(user_id)
if not user:
return self.json({"error": "User not found"}, status=404)
# Verify post exists and belongs to user
post = await self.post_service.get_by_id_and_user(post_id, user_id)
if not post:
return self.json({"error": "Post not found or doesn't belong to user"}, status=404)
return self.render("posts/user_post.html", {
"user": user,
"post": post,
"breadcrumb": [
{"title": "Users", "url": self.generate_url("user.index")},
{"title": user.name, "url": self.generate_url("user.show", id=user.id)},
{"title": "Posts", "url": self.generate_url("user.posts", user_id=user.id)},
{"title": post.title, "url": None}
]
})
@Route("/api/v1/categories/{category}/posts/{post_id}/comments/{comment_id}",
"api.comment.show", methods=["GET"])
async def show_nested_comment(self, category: str, post_id: int, comment_id: int):
"""API endpoint for deeply nested resource access."""
# Validate the entire hierarchy
post = await self.post_service.get_by_id_and_category(post_id, category)
if not post:
return self.json({"error": "Post not found in category"}, status=404)
comment = await self.comment_service.get_by_id_and_post(comment_id, post_id)
if not comment:
return self.json({"error": "Comment not found"}, status=404)
return self.json({
"comment": comment.to_dict(),
"post": {"id": post.id, "title": post.title},
"category": category,
"links": {
"post": self.generate_url("api.post.show", category=category, post_id=post_id),
"all_comments": self.generate_url("api.post.comments", category=category, post_id=post_id)
}
})

Optional Parameters with Defaults:

@Route("/posts", "post.index", methods=["GET"])
@Route("/posts/{category}", "post.by_category", methods=["GET"])
async def posts_by_category(self, category: str = "general"):
"""Display posts, optionally filtered by category."""
# Handle both /posts and /posts/technology URLs
posts = await self.post_service.get_by_category(category)
categories = await self.category_service.get_all()
return self.render("posts/index.html", {
"posts": posts,
"current_category": category,
"categories": categories,
"title": f"Posts in {category.title()}" if category != "general" else "All Posts"
})
@Route("/search", "search.results", methods=["GET"])
async def search(self, q: str = "", page: int = 1, per_page: int = 10):
"""Search with optional pagination parameters."""
if not q:
return self.render("search/form.html", {
"query": "",
"message": "Enter a search term to begin"
})
# Validate pagination parameters
page = max(1, page)
per_page = min(100, max(1, per_page)) # Limit to prevent abuse
results = await self.search_service.search(
query=q,
page=page,
per_page=per_page
)
return self.render("search/results.html", {
"query": q,
"results": results,
"pagination": {
"page": page,
"per_page": per_page,
"total": results.total,
"has_next": results.has_next,
"has_prev": results.has_prev
}
})

Type-Constrained Routes & Advanced Patterns

Section titled “Type-Constrained Routes & Advanced Patterns”

Framefox leverages FastAPI’s powerful type system to provide automatic parameter validation and conversion. This ensures that only valid data types can match routes and provides cleaner error handling.

Supported Type Constraints:

# Integer constraints
@Route("/users/{id:int}", "user.show", methods=["GET"])
async def show_user(self, id: int):
"""Only matches numeric IDs: /users/123 ✅, /users/abc ❌"""
pass
# String constraints (default)
@Route("/posts/{slug:str}", "post.show", methods=["GET"])
async def show_post(self, slug: str):
"""Matches any string: /posts/my-blog-post ✅"""
pass
# Path constraints (captures remaining path)
@Route("/files/{filepath:path}", "file.serve", methods=["GET"])
async def serve_file(self, filepath: str):
"""Matches entire remaining path: /files/docs/guide.pdf ✅"""
# filepath would be "docs/guide.pdf"
return await self.file_service.serve_file(filepath)
# Float constraints
@Route("/api/coordinates/{lat:float}/{lng:float}", "api.location", methods=["GET"])
async def get_location(self, lat: float, lng: float):
"""Matches decimal coordinates: /api/coordinates/40.7128/-74.0060 ✅"""
location_data = await self.geo_service.get_location_info(lat, lng)
return self.json({"location": location_data})

Advanced Path Patterns:

# UUID constraints for secure resource access
from uuid import UUID
@Route("/api/resources/{resource_uuid:uuid}", "api.resource.show", methods=["GET"])
async def show_resource(self, resource_uuid: UUID):
"""Only matches valid UUIDs: prevents ID enumeration attacks."""
resource = await self.resource_service.get_by_uuid(resource_uuid)
if not resource:
return self.json({"error": "Resource not found"}, status=404)
return self.json({"resource": resource.to_dict()})
# Regular expression constraints
@Route("/posts/{year:int}/{month:int}/{day:int}/{slug}", "post.by_date", methods=["GET"])
async def show_post_by_date(self, year: int, month: int, day: int, slug: str):
"""Date-based blog post URLs: /posts/2024/03/15/my-blog-post"""
# Validate date
try:
post_date = datetime(year, month, day)
except ValueError:
return self.json({"error": "Invalid date"}, status=400)
post = await self.post_service.get_by_date_and_slug(post_date, slug)
if not post:
return self.json({"error": "Post not found"}, status=404)
return self.render("posts/show.html", {
"post": post,
"canonical_url": self.generate_url("post.by_date",
year=year, month=month, day=day, slug=slug)
})
# Multiple format support
@Route("/api/data/{format:str}", "api.data.export", methods=["GET"])
async def export_data(self, format: str):
"""Support multiple export formats with validation."""
allowed_formats = ["json", "csv", "xml", "xlsx"]
if format not in allowed_formats:
return self.json({
"error": f"Unsupported format. Allowed: {', '.join(allowed_formats)}"
}, status=400)
data = await self.data_service.get_export_data()
if format == "json":
return self.json({"data": data})
elif format == "csv":
csv_content = await self.export_service.to_csv(data)
return Response(content=csv_content, media_type="text/csv")
elif format == "xml":
xml_content = await self.export_service.to_xml(data)
return Response(content=xml_content, media_type="application/xml")
elif format == "xlsx":
xlsx_content = await self.export_service.to_xlsx(data)
return Response(content=xlsx_content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
# Integer constraint
@Route("/users/{id:int}", "user.show", methods=["GET"])
async def show_user(self, id: int):
# id is guaranteed to be an integer
return {"user_id": id}
# String constraint
@Route("/posts/{slug:str}", "post.show", methods=["GET"])
async def show_post(self, slug: str):
# slug is guaranteed to be a string
return {"post_slug": slug}
# Path constraint (captures slashes)
@Route("/files/{filepath:path}", "file.serve", methods=["GET"])
async def serve_file(self, filepath: str):
# filepath can contain slashes
return {"filepath": filepath}

Framefox supports all standard HTTP methods, allowing you to build RESTful APIs and web applications that follow HTTP conventions.

# GET - Retrieve data
@Route("/users", "user.index", methods=["GET"])
async def get_users(self):
return {"users": []}
# POST - Create new data
@Route("/users", "user.create", methods=["POST"])
async def create_user(self, request: Request):
data = await request.json()
return {"created": True}
# PUT - Complete update
@Route("/users/{id}", "user.update", methods=["PUT"])
async def update_user(self, id: int, request: Request):
return {"updated": True}
# PATCH - Partial update
@Route("/users/{id}", "user.patch", methods=["PATCH"])
async def patch_user(self, id: int, request: Request):
return {"patched": True}
# DELETE - Remove data
@Route("/users/{id}", "user.delete", methods=["DELETE"])
async def delete_user(self, id: int):
return {"deleted": True}

A single method can handle multiple HTTP verbs, useful for form handling or creating flexible endpoints:

@Route("/contact", "contact.form", methods=["GET", "POST"])
async def contact(self, request: Request):
if request.method == "GET":
return self.render("contact/form.html")
elif request.method == "POST":
# Process the form submission
return self.redirect("contact.success")

Consistent route naming makes your application more maintainable and enables powerful features like URL generation and reverse routing:

resource.action
@Route("/users", "user.index", methods=["GET"]) # List all
@Route("/users/create", "user.create", methods=["POST"]) # Creation form
@Route("/users/{id}", "user.show", methods=["GET"]) # Show single
@Route("/users/{id}", "user.update", methods=["PUT"]) # Update
@Route("/users/{id}", "user.patch", methods=["PATCH"]) # Patch
@Route("/users/{id}", "user.delete", methods=["DELETE"]) # Delete

For APIs or modular applications, use prefixed naming to organize routes logically:

# API routes
@Route("/api/users", "api.user.index", methods=["GET"])
@Route("/api/users/{id}", "api.user.show", methods=["GET"])
# Admin routes
@Route("/admin/users", "admin.user.index", methods=["GET"])
@Route("/admin/users/{id}", "admin.user.show", methods=["GET"])
# Versioned API
@Route("/api/v1/posts", "api.v1.post.index", methods=["GET"])
@Route("/api/v2/posts", "api.v2.post.index", methods=["GET"])

Generate URLs dynamically in your controller methods for redirects and responses:

# Redirect to a named route
return self.redirect(self.generate_url("user.show", id=123))
# Generate URL for templates or responses
url = self.generate_url("user.edit", id=user.id)
profile_url = self.generate_url("user.profile", username=user.username)

Use the url_for function in your templates to generate type-safe URLs:

<!-- Simple link -->
<a href="{{ url_for('user.index') }}">All Users</a>
<!-- Link with parameters -->
<a href="{{ url_for('user.show', id=user.id) }}">View {{ user.name }}</a>
<!-- Link with multiple parameters -->
<a href="{{ url_for('user.post.show', user_id=user.id, post_id=post.id) }}">
View Post
</a>
<!-- External links with query parameters -->
<a href="{{ url_for('search.results', q='python', category='programming') }}">
Python Programming
</a>

Query parameters provide additional data to your routes without affecting the URL structure:

from fastapi import Request
@Route("/search", "search.index", methods=["GET"])
async def search(self, request: Request):
query = request.query_params.get("q", "")
page = int(request.query_params.get("page", 1))
per_page = int(request.query_params.get("per_page", 10))
return {
"query": query,
"page": page,
"per_page": per_page,
"results": []
}

For better validation and type safety, use Pydantic models to handle query parameters:

from pydantic import BaseModel
from typing import Optional
class SearchQuery(BaseModel):
q: str = ""
page: int = 1
per_page: int = 10
sort: Optional[str] = None
order: str = "asc"
@Route("/search", "search.index", methods=["GET"])
async def search(self, query: SearchQuery):
return {
"query": query.q,
"page": query.page,
"per_page": query.per_page,
"sort": query.sort,
"order": query.order,
"results": []
}

Use a consistent naming convention across your application:

# ✅ Good - Consistent resource.action pattern
@Route("/users", "user.index", methods=["GET"])
@Route("/users/{id}", "user.show", methods=["GET"])
@Route("/posts", "post.index", methods=["GET"])
@Route("/posts/{id}", "post.show", methods=["GET"])
# ❌ Bad - Inconsistent naming
@Route("/users", "list_users", methods=["GET"])
@Route("/users/{id}", "show_user_detail", methods=["GET"])
@Route("/posts", "all_posts", methods=["GET"])
@Route("/posts/{id}", "post_details", methods=["GET"])

Group related routes in the same controller:

# ✅ Good - Related functionality grouped
class UserController(AbstractController):
# All user-related routes here
pass
class PostController(AbstractController):
# All post-related routes here
pass
# ❌ Bad - Mixed functionality
class MixedController(AbstractController):
# User routes mixed with post routes
pass

Always use type hints for automatic validation:

# ✅ Good - Type validation
@Route("/users/{id}", "user.show", methods=["GET"])
async def show(self, id: int): # FastAPI validates automatically
pass
# ❌ Bad - No validation
@Route("/users/{id}", "user.show", methods=["GET"])
async def show(self, id): # Any value accepted
pass

Implement proper error handling for missing resources:

from fastapi import HTTPException
@Route("/users/{id}", "user.show", methods=["GET"])
async def show(self, id: int):
user = self.user_service.find(id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {"user": user}

Document your routes clearly, especially for APIs:

@Route("/users/{id}", "user.show", methods=["GET"])
async def show(self, id: int):
"""
Retrieve a specific user by ID.
Args:
id: The unique identifier for the user
Returns:
User data object or 404 if not found
Raises:
HTTPException: When user is not found
"""
pass

🔧 How to implement advanced routing patterns and middleware?
🌐 How to build complete RESTful APIs with CRUD operations?
🐛 How to debug routing issues and inspect route behavior?