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
Section titled “The @Route Decorator”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.
Decorator Parameters Deep Dive
Section titled “Decorator Parameters Deep Dive”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)
- Static paths:
-
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
)
- Used for URL generation:
-
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
- Defaults to
Route Types & Patterns
Section titled “Route Types & Patterns”Static Routes
Section titled “Static Routes”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() })
Service Discovery
Section titled “Service Discovery”Services are automatically discovered and registered through Framefox’s comprehensive scanning system:
Discovery Process:
- Framework Core: All modules in
framefox.core.*
are scanned immediately - Controllers: Classes in
src/controller/
are always discovered during registration - Background Scan:
src/service/
andsrc/repository/
are scanned asynchronously - Caching: Discovered services are cached for performance
Exclusions:
- Entity directories (
entity
,entities
,migration
,migrations
) - Test directories (
test
,tests
,__pycache__
) - Static assets and templates
Error Types
Section titled “Error Types”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
Section titled “Parameterized Routes”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 accessfrom 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}
HTTP Methods
Section titled “HTTP Methods”Standard HTTP Methods
Section titled “Standard HTTP Methods”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}
Multiple Methods
Section titled “Multiple Methods”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")
Route Naming
Section titled “Route Naming”Naming Conventions
Section titled “Naming Conventions”Consistent route naming makes your application more maintainable and enables powerful features like URL generation and reverse routing:
@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
Grouped Routes
Section titled “Grouped Routes”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"])
URL Generation
Section titled “URL Generation”In Controllers
Section titled “In Controllers”Generate URLs dynamically in your controller methods for redirects and responses:
# Redirect to a named routereturn self.redirect(self.generate_url("user.show", id=123))
# Generate URL for templates or responsesurl = self.generate_url("user.edit", id=user.id)profile_url = self.generate_url("user.profile", username=user.username)
In Templates
Section titled “In Templates”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
Section titled “Query Parameters”Accessing Query Parameters
Section titled “Accessing Query Parameters”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": [] }
Using Pydantic Models
Section titled “Using Pydantic Models”For better validation and type safety, use Pydantic models to handle query parameters:
from pydantic import BaseModelfrom 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": [] }
Route Organization
Section titled “Route Organization”Best Practices
Section titled “Best Practices”1. Consistent Naming
Section titled “1. Consistent Naming”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"])
2. Logical Controller Organization
Section titled “2. Logical Controller Organization”Group related routes in the same controller:
# ✅ Good - Related functionality groupedclass UserController(AbstractController): # All user-related routes here pass
class PostController(AbstractController): # All post-related routes here pass
# ❌ Bad - Mixed functionalityclass MixedController(AbstractController): # User routes mixed with post routes pass
3. Parameter Validation
Section titled “3. Parameter Validation”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
4. Error Handling
Section titled “4. Error Handling”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}
5. Documentation
Section titled “5. Documentation”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
Related Topics
Section titled “Related Topics”🔧 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?