Introduction
Routing in Framefox maps HTTP requests to controller methods using the @Route decorator. The routing system is built on top of FastAPI’s routing capabilities with additional features for automatic controller discovery and lazy loading.
The @Route Decorator
The @Route decorator is the primary way to define routes in Framefox:
/home/daytona/workspace/source/framefox/core/routing/decorator/route.py:15-22
class Route :
def __init__ ( self , path : str , name : str , methods : list , response_model = None , tags = None ):
self .path = path
self .name = name
self .methods = methods
self .response_model = response_model
self .tags = tags or []
Basic Route
Define a simple route in your controller:
from framefox.core.controller.abstract_controller import AbstractController
from framefox.core.routing.decorator.route import Route
class HomeController ( AbstractController ):
@Route ( "/" , "home.index" , methods = [ "GET" ])
async def index ( self ):
return self .render( "home/index.html" , { "title" : "Welcome" })
Route Parameters
path (str): The URL path pattern (e.g., "/users", "/posts/{id}")
name (str): Unique route name for URL generation (e.g., "user.list", "post.detail")
methods (list): HTTP methods (e.g., ["GET"], ["POST", "PUT"])
response_model (optional): Pydantic model for response validation
tags (optional): Tags for OpenAPI documentation grouping
Path Parameters
Define dynamic URL segments using curly braces:
class UserController ( AbstractController ):
@Route ( "/users/ {user_id} " , "user.detail" , methods = [ "GET" ])
async def detail ( self , user_id : int ):
# user_id is automatically extracted from URL
user = # fetch user by id
return self .render( "user/detail.html" , { "user" : user})
Multiple Path Parameters
@Route ( "/posts/ {post_id} /comments/ {comment_id} " , "comment.detail" , methods = [ "GET" ])
async def comment_detail ( self , post_id : int , comment_id : int ):
return self .json({
"post_id" : post_id,
"comment_id" : comment_id
})
Type Conversion
Path parameters are automatically converted based on type hints:
# Integer parameter
@Route ( "/posts/ {post_id} " , "post.show" , methods = [ "GET" ])
async def show ( self , post_id : int ):
# post_id is an int
...
# String parameter (default)
@Route ( "/users/ {username} " , "user.profile" , methods = [ "GET" ])
async def profile ( self , username : str ):
# username is a str
...
The Route decorator automatically detects path parameters and ensures they’re passed correctly to your method.
HTTP Methods
Single Method Routes
@Route ( "/users" , "user.list" , methods = [ "GET" ])
async def list ( self ):
return self .render( "user/list.html" , { "users" : users})
@Route ( "/users/create" , "user.create" , methods = [ "POST" ])
async def create ( self ):
# Handle POST request
return self .redirect( "/users" )
Multiple Method Routes
A single method can handle multiple HTTP methods:
@Route ( "/users/ {id} " , "user.edit" , methods = [ "GET" , "POST" ])
async def edit ( self , id : int , request : Request):
if request.method == "GET" :
# Show edit form
return self .render( "user/edit.html" , { "user" : user})
else :
# Handle form submission
return self .redirect( "/users" )
REST-style Routes
class PostController ( AbstractController ):
@Route ( "/api/posts" , "api.post.list" , methods = [ "GET" ])
async def list ( self ):
return self .json({ "posts" : posts})
@Route ( "/api/posts/ {id} " , "api.post.show" , methods = [ "GET" ])
async def show ( self , id : int ):
return self .json({ "post" : post})
@Route ( "/api/posts" , "api.post.create" , methods = [ "POST" ])
async def create ( self ):
return self .json({ "post" : new_post}, status = 201 )
@Route ( "/api/posts/ {id} " , "api.post.update" , methods = [ "PUT" ])
async def update ( self , id : int ):
return self .json({ "post" : updated_post})
@Route ( "/api/posts/ {id} " , "api.post.delete" , methods = [ "DELETE" ])
async def delete ( self , id : int ):
return self .json({ "message" : "Deleted" }, status = 204 )
Query Parameters
Query parameters are automatically extracted from the URL:
@Route ( "/search" , "search.index" , methods = [ "GET" ])
async def search ( self , q : str = "" , page : int = 1 , limit : int = 10 ):
# Access query parameters: /search?q=python&page=2&limit=20
results = # search with q, page, limit
return self .render( "search/results.html" , {
"results" : results,
"query" : q,
"page" : page
})
Query parameters with default values become optional. Without defaults, they’re required.
Request Body & Forms
JSON Request Body
Use Pydantic models for request validation:
from pydantic import BaseModel
class UserCreate ( BaseModel ):
username: str
email: str
password: str
class UserController ( AbstractController ):
@Route ( "/api/users" , "api.user.create" , methods = [ "POST" ])
async def create ( self , user_data : UserCreate):
# user_data is validated automatically
return self .json({
"username" : user_data.username,
"email" : user_data.email
})
from fastapi import Form
@Route ( "/login" , "auth.login" , methods = [ "POST" ])
async def login ( self , username : str = Form( ... ), password : str = Form( ... )):
# Handle form submission
return self .redirect( "/dashboard" )
URL Generation
Generate URLs from route names to avoid hardcoding:
In Controllers
Use the generate_url() method:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:53-65
def generate_url ( self , route_name : str , ** params ):
"""
Generates a URL for a named route with optional parameters.
Args:
route_name (str): The name of the route to generate URL for
**params: Additional parameters to include in the URL
Returns:
str: The generated URL
"""
router = self ._get_container().get_by_name( "Router" )
return router.url_path_for(route_name, ** params)
Example:
@Route ( "/users/ {id} /profile" , "user.profile" , methods = [ "GET" ])
async def profile ( self , id : int ):
edit_url = self .generate_url( "user.edit" , id = id )
return self .render( "user/profile.html" , {
"user" : user,
"edit_url" : edit_url # /users/123/edit
})
In Templates
Generate URLs in Jinja2 templates:
{# Direct path #}
< a href = "/users/123" > User Profile </ a >
{# Better: Use url_for (if available) #}
< a href = "{{ url_for('user.profile', id=user.id) }}" > User Profile </ a >
Router Methods
Access the Router directly:
/home/daytona/workspace/source/framefox/core/routing/router.py:374-396
def url_path_for ( self , name : str , ** params ) -> str :
"""Generate URL for a named route with parameters"""
if name not in self ._routes:
self ._logger.warning( f "Route ' { name } ' not found in registered routes" )
return "#"
try :
route_path = self ._routes[name]
for key, value in params.items():
route_path = route_path.replace( f " {{ { key } }} " , str (value))
return route_path
except Exception as e:
self ._logger.error( f "Failed to generate URL for route ' { name } ': { e } " )
return "#"
def get_registered_routes ( self ) -> Dict[ str , str ]:
"""Get a copy of all registered routes"""
return self ._routes.copy()
def route_exists ( self , name : str ) -> bool :
"""Check if a route exists"""
return name in self ._routes
Automatic Dependency Injection
The Route decorator automatically injects services into controller methods:
/home/daytona/workspace/source/framefox/core/routing/decorator/route.py:28-58
@wraps (func)
async def wrapper ( * args , ** kwargs ):
controller_instance = args[ 0 ] if args else None
if controller_instance and hasattr (controller_instance, "_container" ):
for param_name, param in original_sig.parameters.items():
if param_name == "self" or param_name in kwargs:
continue
param_type = type_hints.get(param_name)
if param_type and param_type != type ( None ):
if (
self ._is_fastapi_native_type(param_type)
or self ._is_pydantic_model(param_type)
or self ._is_primitive_type(param_type)
or self ._is_path_parameter(param_name)
):
continue
try :
service = controller_instance._container.get(param_type)
kwargs[param_name] = service
except Exception as e:
if param.default != inspect.Parameter.empty:
kwargs[param_name] = param.default
else :
logger = logging.getLogger( "ROUTE" )
logger.error( f "Dependency injection failed for { param_type. __name__ } in { func. __name__ } . { param_name } : { e } " )
raise RuntimeError ( f "Dependency injection failed for { param_type. __name__ } " )
return await func( * args, ** kwargs)
Service Injection Example
from framefox.core.orm.entity_manager_interface import EntityManagerInterface
from framefox.core.logging.logger import Logger
class ProductController ( AbstractController ):
@Route ( "/products" , "product.list" , methods = [ "GET" ])
async def list ( self , em : EntityManagerInterface, logger : Logger):
# Services automatically injected!
logger.info( "Fetching products" )
products = em.find_all(Product)
return self .render( "product/list.html" , { "products" : products})
Mixed Parameters
Combine path parameters, query parameters, request objects, and services:
from fastapi import Request
@Route ( "/posts/ {post_id} /edit" , "post.edit" , methods = [ "GET" , "POST" ])
async def edit (
self ,
post_id : int , # Path parameter
request : Request, # FastAPI Request object
em : EntityManagerInterface, # Injected service
logger : Logger, # Injected service
preview : bool = False # Query parameter
):
logger.info( f "Editing post { post_id } " )
post = em.find(Post, post_id)
if request.method == "POST" :
# Handle form submission
return self .redirect( self .generate_url( "post.show" , id = post_id))
return self .render( "post/edit.html" , {
"post" : post,
"preview" : preview
})
Controller Discovery
Framefox automatically discovers controllers in the src/controller/ directory:
/home/daytona/workspace/source/framefox/core/routing/router.py:139-167
def _register_user_controllers ( self ):
controllers_path = Path( "src/controller" )
if not controllers_path.exists():
self ._logger.debug( "No user controllers directory found" )
return
registered_count = 0
for controller_file in controllers_path.rglob( "*.py" ):
if controller_file.name == "__init__.py" :
continue
try :
module_name = self ._get_module_name(controller_file)
controller_classes = self ._discover_controller_classes(module_name)
for controller_class in controller_classes:
controller_name = controller_class. __name__ .replace( "Controller" , "" ).lower()
def create_lazy_factory ( name : str ):
return lambda : self .controller_resolver.resolve_controller(name)
lazy_factory = create_lazy_factory(controller_name)
self ._register_controller_routes(controller_class, lazy_factory = lazy_factory)
registered_count += 1
self ._logger.debug( f "Registered lazy controller: { controller_class. __name__ } " )
except Exception as e:
self ._logger.error( f "Failed to register controller { controller_file } : { e } " )
continue
Controller Organization
src/controller/
├── home_controller.py
├── user_controller.py
├── post_controller.py
└── admin/
├── dashboard_controller.py
└── settings_controller.py
Complete Example
Here’s a complete controller demonstrating various routing features:
/home/daytona/workspace/source/framefox/core/debug/profiler/profiler_controller.py:16-64
class ProfilerController ( AbstractController ):
"""
Web profiler controller for Framefox.
Handles request profiling information display.
"""
def __init__ ( self ):
self .profiler = Profiler()
@Route ( "/_profiler" , "profiler.index" , methods = [ "GET" ])
async def profiler_index ( self , page : int = 1 , limit : int = 50 ):
profiles = self .profiler.list_profiles( limit = limit, page = page)
total_count = len ( self .profiler.list_profiles( limit = 10000 ))
return self .render(
"profiler/index.html" ,
{
"profiles" : profiles,
"page" : page,
"limit" : limit,
"total" : total_count,
"page_count" : (total_count + limit - 1 ) // limit,
},
)
@Route ( "/_profiler/ {token} " , "profiler.detail" , methods = [ "GET" ])
async def profiler_detail ( self , token : str ):
profile = self .profiler.get_profile(token)
return self .render( "profiler/details.html" , { "token" : token, "profile" : profile})
@Route ( "/_profiler/ {token} / {panel} " , "profiler.panel" , methods = [ "GET" ])
async def profiler_panel ( self , token : str , panel : str ):
profile = self .profiler.get_profile(token)
panel_data = profile.get(panel, {})
template_context = {
"token" : token,
"panel" : panel,
"data" : panel_data,
"profile" : profile,
}
return self .render( f "profiler/panels/ { panel } .html" , template_context)
@Route ( "/_profiler/ {token} /json" , "profiler.json" , methods = [ "GET" ])
async def profiler_json ( self , token : str ):
profile = self .profiler.get_profile(token)
return JSONResponse( content = profile)
Best Practices
Use Meaningful Route Names
Choose descriptive route names that follow a consistent pattern: # Good
@Route ( "/users" , "user.list" , methods = [ "GET" ])
@Route ( "/users/ {id} " , "user.detail" , methods = [ "GET" ])
@Route ( "/users/create" , "user.create" , methods = [ "POST" ])
# Bad
@Route ( "/users" , "route1" , methods = [ "GET" ])
@Route ( "/users/ {id} " , "show_user" , methods = [ "GET" ])
@Route ( "/users/create" , "user_creation" , methods = [ "POST" ])
Follow REST conventions for API endpoints: GET / api / posts # List
GET / api / posts / { id } # Show
POST / api / posts # Create
PUT / api / posts / { id } # Update
DELETE / api / posts / { id } # Delete
Always use type hints for path parameters and dependency injection: # Good
@Route ( "/posts/ {id} " , "post.show" , methods = [ "GET" ])
async def show ( self , id : int , em : EntityManagerInterface):
...
# Bad - no type information
@Route ( "/posts/ {id} " , "post.show" , methods = [ "GET" ])
async def show ( self , id , em ):
...
Generate URLs Programmatically
Never hardcode URLs - use route names: # Good
redirect_url = self .generate_url( "user.profile" , id = user.id)
# Bad
redirect_url = f "/users/ { user.id } /profile"
OpenAPI Documentation
Framefox automatically generates OpenAPI documentation for your routes:
@Route (
"/api/users" ,
"api.user.list" ,
methods = [ "GET" ],
response_model = UserListResponse, # Pydantic model for response
tags = [ "Users" ] # Groups in OpenAPI docs
)
async def list_users ( self ):
...
Access the interactive documentation at:
Swagger UI : http://localhost:8000/docs
ReDoc : http://localhost:8000/redoc
OpenAPI documentation is only available in development mode (app_env=dev).
Next Steps
MVC Pattern Learn about controllers and views
Dependency Injection Understand service injection in routes
Request Handling Work with requests and responses
API Development Build RESTful APIs with Framefox