sillo provides a powerful and flexible routing system that supports path parameters, query parameters, and various HTTP methods. The routing system is designed to be intuitive, performant, and extensible.
Routing
Section titled “Routing”sillo provides a powerful and flexible routing system that supports using decorators to define routes or using the Route class. The routing system is designed to be intuitive, performant, and extensible, making it easy to define routes and handle requests.
When a path contains a {name} segment, the matched value is bound to a handler parameter of the same name — this is the recommended way to read path parameters. The same values are also available on request.path_params if you prefer to read them imperatively. Both styles work; binding by name keeps the handler signature explicit.
Using decorators
Section titled “Using decorators”sillo provides a simple and intuitive way to define routes using decorators. You can use the @app.get, @app.post, @app.put, @app.delete, @app.head, and @app.options etc decorators to define routes.
from sillo import silloApp
app = silloApp()
@app.get("/")async def index(request, response): return response.json({"message": "Hello"})
@app.post("/items")async def create_item(request, response): data = await request.json return response.json(data, status_code=201)More Examples
from sillo import silloApp
app = silloApp()
@app.get("/")async def index(request, response): return response.json({"message": "Hello"})
@app.post("/items")async def create_item(request, response): data = await request.json return response.json(data, status_code=201)
@app.put("/items/{id}")async def update_item(request, response, id: str): data = await request.json return response.json({"id": id, **data})
@app.delete("/items/{id}")async def delete_item(request, response, id: str): return response.json(None, status_code=204)
# If a required path parameter is missing or invalid, sillo will return a 422 error.# For example, GET /items/abc (when id is expected as int) will return a validation error.
# If you define two routes with the same path and method, sillo will raise a conflict error at startup.@app.route("/items", methods=["GET", "POST"])async def handle_items(request, response): if request.method == "GET": return response.json({"items": []}) elif request.method == "POST": data = await request.json return response.json(data, status_code=201)
# If you send a request with an unsupported method, sillo will return a 405 Method Not Allowed.@app.head("/status")async def status(request, response): response.set_header("X-API-Version", "1.0") return response.json(None)
@app.options("/items")async def items_options(request, response): response.set_header("Allow", "GET, POST, PUT, DELETE") return response.json(None)
# If you forget to return a response, sillo will raise an error indicating the handler did not return a response object.Using Route class and add_route method
Section titled “Using Route class and add_route method”sillo also provides a Route class that allows you to define routes in a more structured way.
It’s especially useful when you have a lot of routes and want to organize them in a logical manner.
from sillo import silloAppfrom sillo.routing import Route
app = silloApp()
async def get_user_handler(request, response, user_id): return response.json({"user_id": user_id})route = Route( path="/users/{user_id:int}", handler=get_user_handler, methods=["GET"], name="get_user", summary="Get user by ID", description="Retrieves a user by their unique identifier")
app.add_route(route)The Route class is the fundamental building block of sillo routing. It encapsulates all routing information for an API endpoint, including path handling, validation, OpenAPI documentation, and request processing.
from sillo.routing import Route
# Basic route creationroute = Route( path="/users/{user_id:int}", handler=get_user_handler, methods=["GET"], name="get_user", summary="Get user by ID", description="Retrieves a user by their unique identifier")Route Class Constructor
Section titled “Route Class Constructor”The Route constructor is used to define a route within the sillo application. It takes several parameters:
- path: A string that specifies the URL path pattern. It can include path parameters with type annotations, like
/users/{user_id:int}. - handler: An optional request handler function that processes incoming requests matching the route.
- methods: A list of HTTP methods (e.g.,
["GET", "POST"]) that the route accepts. If not specified, it defaults to["GET"]. - name: An optional name for the route, used for URL generation.
- summary: A brief description of the endpoint, useful for documentation.
- description: A detailed explanation of the endpoint’s functionality.
- responses: A dictionary that maps HTTP status codes to response schemas.
- request_model: An optional Pydantic model for validating and parsing request data.
- middleware: A list of middleware functions specific to the route.
- tags: A list of OpenAPI tags for categorizing the endpoint in documentation.
- security: A list of security requirements for accessing the route.
- operation_id: A unique identifier for the operation, useful for documentation and client generation.
- deprecated: A boolean indicating whether the route is deprecated.
- parameters: A list of additional OpenAPI parameters for the route.
- exclude_from_schema: A boolean indicating whether to exclude the route from OpenAPI documentation.
- kwargs: Additional metadata for the route.
Route( path: str, # URL path pattern handler: Optional[HandlerType] = None, # Request handler function methods: Optional[List[str]] = None, # HTTP methods (default: ["GET"]) name: Optional[str] = None, # Route name for URL generation summary: Optional[str] = None, # Brief endpoint summary description: Optional[str] = None, # Detailed endpoint description responses: Optional[Dict[int, Any]] = None, # Response schemas by status code request_model: Optional[Type[BaseModel]] = None, # Pydantic model for validation middleware: List[Any] = [], # Route-specific middleware tags: Optional[List[str]] = None, # OpenAPI tags for grouping security: Optional[List[Dict[str, List[str]]]] = None, # Security requirements operation_id: Optional[str] = None, # Unique operation identifier deprecated: bool = False, # Mark as deprecated parameters: List[Parameter] = [], # Additional OpenAPI parameters exclude_from_schema: bool = False, # Hide from OpenAPI docs **kwargs: Dict[str, Any] # Additional metadata)Creating and Using Routers
Section titled “Creating and Using Routers”Routers allow you to organize related routes and apply common configuration:
from sillo.routing import Router
# Create routers for different API versionsv1_router = Router(prefix="/api/v1", tags=["API v1"])v2_router = Router(prefix="/api/v2", tags=["API v2"])
# Add routes to v1 router@v1_router.get("/users")async def list_users_v1(request, response): return response.json({"version": "v1", "users": []})
@v1_router.post("/users")async def create_user_v1(request, response): data = await request.json return response.json({"version": "v1", "user": data}, status_code=201)
# Add routes to v2 router@v2_router.get("/users")async def list_users_v2(request, response): return response.json({"version": "v2", "users": []})
@v2_router.post("/users")async def create_user_v2(request, response): data = await request.json return response.json({"version": "v2", "user": data}, status_code=201)
# Mount routers to main appapp.mount_router(v1_router)app.mount_router(v2_router)Nested Routers
Section titled “Nested Routers”You can create nested routers for complex API structures:
# Create main API routerapi_router = Router(prefix="/api", tags=["API"])
# Create version-specific routersv1_router = Router(prefix="/v1", tags=["v1"])v2_router = Router(prefix="/v2", tags=["v2"])
# Create resource-specific routersusers_router = Router(prefix="/users", tags=["Users"])posts_router = Router(prefix="/posts", tags=["Posts"])
# Add routes to resource routers@users_router.get("/")async def list_users(request, response): return response.json({"users": []})
@users_router.get("/{user_id:int}")async def get_user(request, response, user_id: int): return response.json({"id": user_id})
@posts_router.get("/")async def list_posts(request, response): return response.json({"posts": []})
@posts_router.get("/{post_id:int}")async def get_post(request, response, post_id: int): return response.json({"id": post_id})
# Mount resource routers to version routersv1_router.mount_router(users_router)v1_router.mount_router(posts_router)v2_router.mount_router(users_router)v2_router.mount_router(posts_router)
# Mount version routers to API routerapi_router.mount_router(v1_router)api_router.mount_router(v2_router)
# Mount API router to main appapp.mount_router(api_router)This creates the following URL structure:
/api/v1/users/- List users (v1)/api/v1/users/{user_id}- Get user (v1)/api/v1/posts/- List posts (v1)/api/v1/posts/{post_id}- Get post (v1)/api/v2/users/- List users (v2)/api/v2/users/{user_id}- Get user (v2)/api/v2/posts/- List posts (v2)/api/v2/posts/{post_id}- Get post (v2)
Router with Middleware
Section titled “Router with Middleware”You can apply middleware to all routes in a router:
from sillo.middleware import CORSMiddleware
# Create router with middlewareadmin_router = Router( prefix="/admin", tags=["Admin"], middleware=[CORSMiddleware()])
@admin_router.get("/dashboard")async def admin_dashboard(request, response): return response.json({"dashboard": "data"})
@admin_router.get("/users")async def admin_users(request, response): return response.json({"users": []})
# All routes in admin_router will have CORS middleware appliedapp.mount_router(admin_router)
# If your middleware raises an exception, the request will be interrupted and a 500 error will be returned. Use try/except in middleware for graceful error handling.Router Class Constructor
Section titled “Router Class Constructor”Router( prefix: Optional[str] = None, # URL prefix for all routes routes: Optional[List[Route]] = None, # Initial routes to add tags: Optional[List[str]] = None, # Default tags for all routes exclude_from_schema: bool = False, # Hide all routes from docs name: Optional[str] = None # Router name)Dynamic Route
Section titled “Dynamic Route”A dynamic route in sillo is a route pattern that can capture parts of the URL as variables, similar to how Express.js or FastAPI handle route parameters.
Basic Concept When you define a route like:
from sillo import silloApp
app = silloApp()
@app.get("/users/{user_id}")async def get_user(request, response, user_id): return {"id": user_id}This route will match URLs like:
/users/12/users/abc123
and automatically pass the part inside {} (user_id here) as an argument to your function.
sillo provides several built-in path converters for validating and converting URL parameters:
you can also get the dynamic params as direct function parameters.
from sillo import silloApp
app = silloApp()
@app.get("/users/{user_id}")async def get_user(request, response, user_id: str): return {"id": user_id}Route Converters in sillo
Section titled “Route Converters in sillo”By default, parameters are strings, but converters allow you to enforce/convert the parameter to a type, or alter what the pattern matches. The syntax is:
Route("/users/{user_id:int}", handler) Route("/files/{full_path:path}", handler)Examples
@app.get("/users/{user_id:int}")async def get_user(request, response, user_id: int): return response.json({"id": user_id})
# If user_id is not an integer, sillo will return a 422 error.
@app.get("/files/{filename:str}")async def get_file(request, response, filename: str): return response.json({"file": filename})
@app.get("/items/{item_id:uuid}")async def get_item(request, response, item_id: str): return response.json({"id": str(item_id)})
# If item_id is not a valid UUID, sillo will return a 422 error.@app.get("/static/{filepath:path}")async def get_static_file(request, response, filepath: str): return response.json({"path": filepath})
@app.get("/posts/{slug:slug}")async def get_post(request, response, slug: str): return response.json({"slug": slug})
# If the slug does not match the expected pattern, sillo will return a 422 error.@app.get("/products/{price:float}")async def get_product(request, response, price: float): return response.json({"price": price})
@app.get("/orders/{order_id:int}")async def get_order(request, response, order_id: int): return response.json({"order_id": order_id})
# If price or order_id are not valid numbers, sillo will return a 422 error.Available Converters
Section titled “Available Converters”| Converter | Type | Pattern | Description |
|---|---|---|---|
str | String | [^/]+ | Any string without slashes |
path | String | .* | Any string including slashes |
int | Integer | [0-9]+ | Positive integers |
float | Float | [0-9]+(\.[0-9]+)? | Positive floats |
uuid | UUID | [0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12} | UUID format |
slug | String | [a-z0-9]+(?:-[a-z0-9]+)* | URL-friendly strings |
Custom Path Converters
Section titled “Custom Path Converters”You can create and register custom path converters by subclassing the Convertor class:
from sillo.converters import Convertor, register_url_convertorimport re
class EmailConvertor(Convertor[str]): regex = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
def convert(self, value: str) -> str: if not re.fullmatch(self.regex, value): raise ValueError(f"Invalid email format: {value}") return value
def to_string(self, value: str) -> str: if not re.fullmatch(self.regex, value): raise ValueError(f"Invalid email format: {value}") return value
# Register the custom converterregister_url_convertor("email", EmailConvertor())
# Use the custom converter in routes@app.get("/users/{email:email}")async def get_user_by_email(request, response, email: str): return response.json({"email": email})
# If your custom converter raises a ValueError, sillo will return a 422 error with your message.Creating Custom Converters
Section titled “Creating Custom Converters”To create a custom converter:
- Subclass
Convertorwith the desired type:
class MyConvertor(Convertor[YourType]): regex = "your-regex-pattern"-
Implement the required methods:
convert(self, value: str) -> YourType: Converts string to your typeto_string(self, value: YourType) -> str: Converts your type to string
-
Register the converter:
register_url_convertor("converter_name", MyConvertor())Example: Version Converter
Section titled “Example: Version Converter”class VersionConvertor(Convertor[str]): regex = r"v[0-9]+(\.[0-9]+)*"
def convert(self, value: str) -> str: if not re.fullmatch(self.regex, value): raise ValueError(f"Invalid version format: {value}") return value
def to_string(self, value: str) -> str: if not re.fullmatch(self.regex, value): raise ValueError(f"Invalid version format: {value}") return value
register_url_convertor("version", VersionConvertor())
@app.get("/api/{version:version}/users")async def get_users(request, response, version: str): return response.json({"version": version})Best Practices
When creating custom converters:
- Use clear and efficient regex patterns
- Validate input in both
convertandto_stringmethods - Provide meaningful error messages
- Consider performance implications
- Test thoroughly with edge cases
Route Metadata and Documentation
Section titled “Route Metadata and Documentation”Using Pydantic Models for Responses
Section titled “Using Pydantic Models for Responses”sillo provides excellent integration with Pydantic models for response documentation and validation. Using Pydantic models instead of dictionaries provides:
- Type Safety: Compile-time type checking
- Automatic Documentation: OpenAPI schemas are generated automatically
- Validation: Response data can be validated against the model
- IDE Support: Better autocomplete and error detection
- Consistency: Standardized response formats across your API
Basic Response Models
Section titled “Basic Response Models”from pydantic import BaseModel, EmailStrfrom typing import Optional, Listfrom datetime import datetime
# Basic response modelsclass UserResponse(BaseModel): id: int name: str email: EmailStr age: Optional[int] = None created_at: datetime is_active: bool = True
class UserListResponse(BaseModel): users: List[UserResponse] total: int page: int per_page: int
class ErrorResponse(BaseModel): error: str code: str details: Optional[dict] = None timestamp: datetime = Field(default_factory=datetime.utcnow)
class SuccessResponse(BaseModel): message: str data: Optional[dict] = None
# Use in routes@app.get( "/users/{user_id:int}", responses={ 200: UserResponse, 404: ErrorResponse })async def get_user(request, response, user_id: int): user = await get_user_from_db(user_id) return response.json(user)
@app.get( "/users", responses={ 200: UserListResponse, 400: ErrorResponse })async def list_users(request, response): return response.json({ "users": [ { "id": 1, "name": "John Doe", "email": "john@example.com", "created_at": datetime.utcnow(), "is_active": True } ], "total": 1, "page": 1, "per_page": 10 })Advanced Response Models
Section titled “Advanced Response Models”from pydantic import BaseModel, Field, validatorfrom typing import Union, Literal
# Union types for different response scenariosclass UserCreatedResponse(BaseModel): id: int name: str email: EmailStr message: Literal["User created successfully"]
class UserUpdatedResponse(BaseModel): id: int name: str email: EmailStr message: Literal["User updated successfully"]
class UserDeletedResponse(BaseModel): message: Literal["User deleted successfully"] deleted_id: int
# Generic response wrapperclass ApiResponse(BaseModel): success: bool data: Optional[dict] = None error: Optional[str] = None meta: Optional[dict] = None
# Use union types for different status codes@app.post( "/users", responses={ 201: UserCreatedResponse, 400: ErrorResponse, 409: ErrorResponse })async def create_user(request, response): data = await request.json # ... create user logic return response.json({ "id": 1, "name": data["name"], "email": data["email"], "message": "User created successfully" }, status_code=201)
@app.delete( "/users/{user_id:int}", responses={ 200: UserDeletedResponse, 404: ErrorResponse })async def delete_user(request, response, user_id: int): # ... delete user logic return response.json({ "message": "User deleted successfully", "deleted_id": user_id })Response Model Inheritance
Section titled “Response Model Inheritance”# Base models for common fieldsclass BaseUser(BaseModel): id: int name: str email: EmailStr
class BaseResponse(BaseModel): success: bool message: str
# Inherit from base modelsclass UserDetailResponse(BaseUser): age: Optional[int] = None bio: Optional[str] = None created_at: datetime updated_at: datetime
class UserSummaryResponse(BaseUser): is_active: bool
class ApiSuccessResponse(BaseResponse): data: Optional[dict] = None
class ApiErrorResponse(BaseResponse): error_code: str details: Optional[dict] = None
# Use inherited models@app.get( "/users/{user_id:int}", responses={ 200: UserDetailResponse, 404: ApiErrorResponse })async def get_user_detail(request, response, user_id: int): # ... implementation pass
@app.get( "/users", responses={ 200: List[UserSummaryResponse], 400: ApiErrorResponse })async def list_users_summary(request, response): # ... implementation passResponse Model with Computed Fields
Section titled “Response Model with Computed Fields”from pydantic import BaseModel, computed_field
class UserWithStats(BaseModel): id: int name: str email: EmailStr posts_count: int followers_count: int
@computed_field @property def total_engagement(self) -> int: return self.posts_count + self.followers_count
@computed_field @property def engagement_rate(self) -> float: return self.total_engagement / max(self.followers_count, 1)
@app.get( "/users/{user_id:int}/stats", responses={ 200: UserWithStats, 404: ErrorResponse })async def get_user_stats(request, response, user_id: int): # ... fetch user stats return response.json({ "id": user_id, "name": "John Doe", "email": "john@example.com", "posts_count": 25, "followers_count": 1000 # total_engagement and engagement_rate are computed automatically })OpenAPI Integration
Section titled “OpenAPI Integration”sillo automatically generates OpenAPI documentation from your routes:
from pydantic import BaseModelfrom typing import Optional
class UserResponse(BaseModel): id: int name: str email: str age: Optional[int] = None
class ErrorResponse(BaseModel): error: str code: str details: Optional[dict] = None
@app.get( "/users/{user_id:int}", name="get_user", summary="Get user by ID", description="Retrieves a user by their unique identifier. Returns user details including profile information.", tags=["Users"], responses={ 200: UserResponse, 404: ErrorResponse, 500: ErrorResponse }, deprecated=False)async def get_user(request, response, user_id: int): user = await get_user_from_db(user_id) return response.json(user)Security Requirements
Section titled “Security Requirements”You can specify security requirements for routes:
@app.get( "/admin/users", security=[{"BearerAuth": []}], tags=["Admin"], summary="List all users (Admin only)", responses={ 200: list[UserResponse], 401: ErrorResponse, 403: ErrorResponse })async def admin_list_users(request, response): return response.json({"users": []})
@app.post( "/users/login", security=[], # No security required tags=["Authentication"], summary="User login", responses={ 200: {"token": str}, 401: ErrorResponse })async def login(request, response): return response.json({"token": "jwt_token"})URL Generation
Section titled “URL Generation”Using Route Names
Section titled “Using Route Names”You can generate URLs using route names:
@app.get("/users/{user_id:int}", name="get_user")async def get_user(request, response, user_id: int): return response.json({"id": user_id})
@app.get("/posts/{post_id:int}", name="get_post")async def get_post(request, response, post_id: int): return response.json({"id": post_id})
# Generate URLsuser_url = app.url_for("get_user", user_id=123)post_url = app.url_for("get_post", post_id=456)
print(user_url) # /users/123print(post_url) # /posts/456URL Generation with Query Parameters
Section titled “URL Generation with Query Parameters”from sillo.objects import URLPath
@app.get("/search", name="search")async def search(request, response): query = request.query_params.get("q", "") return response.json({"query": query})
# Generate URL with query parameterssearch_url = app.url_for("search", q="python", page=1)print(search_url) # /search?q=python&page=1
# You can also build URLs manuallyurl = URLPath("/users/123")url = url.add_query_params(page=1, limit=10)print(url) # /users/123?page=1&limit=10Advanced Routing Patterns
Section titled “Advanced Routing Patterns”Route Factories
Section titled “Route Factories”You can create routes programmatically:
def create_crud_routes(resource_name: str, model_class): """Create CRUD routes for a resource"""
routes = []
# List route async def list_handler(request, response): items = await model_class.all() return response.json({f"{resource_name}": items})
routes.append(Route( path=f"/{resource_name}", handler=list_handler, methods=["GET"], name=f"list_{resource_name}", summary=f"List all {resource_name}", tags=[resource_name.title()], responses={ 200: List[model_class.ResponseModel], 400: ErrorResponse } ))
# Create route async def create_handler(request, response): data = await request.json item = await model_class.create(**data) return response.json(item, status_code=201)
routes.append(Route( path=f"/{resource_name}", handler=create_handler, methods=["POST"], name=f"create_{resource_name}", summary=f"Create new {resource_name}", tags=[resource_name.title()], responses={ 201: model_class.ResponseModel, 400: ErrorResponse, 409: ErrorResponse } ))
# Get route async def get_handler(request, response, id: str): item = await model_class.get(id) return response.json(item)
routes.append(Route( path=f"/{resource_name}/{{id:int}}", handler=get_handler, methods=["GET"], name=f"get_{resource_name}", summary=f"Get {resource_name} by ID", tags=[resource_name.title()], responses={ 200: model_class.ResponseModel, 404: ErrorResponse } ))
return routes
# Example model class with response modelclass UserModel: class ResponseModel(BaseModel): id: int name: str email: str created_at: datetime
@classmethod async def all(cls): # Implementation pass
@classmethod async def create(cls, **data): # Implementation pass
@classmethod async def get(cls, id: int): # Implementation pass
# Use the factoryuser_routes = create_crud_routes("users", UserModel)post_routes = create_crud_routes("posts", PostModel)
# Add all routesfor route in user_routes + post_routes: app.add_route(route)Dynamic Route Registration
Section titled “Dynamic Route Registration”You can register routes dynamically:
# Load routes from configurationroutes_config = [ { "path": "/api/v1/users", "methods": ["GET"], "handler": "user_handlers.list_users", "name": "list_users" }, { "path": "/api/v1/users/{user_id:int}", "methods": ["GET"], "handler": "user_handlers.get_user", "name": "get_user" }]
# Register routes dynamicallyfor route_config in routes_config: # Import handler dynamically module_name, handler_name = route_config["handler"].rsplit(".", 1) module = __import__(module_name, fromlist=[handler_name]) handler = getattr(module, handler_name)
route = Route( path=route_config["path"], handler=handler, methods=route_config["methods"], name=route_config["name"] )
app.add_route(route)
# If a dynamically imported handler does not exist or fails to import, sillo will raise an ImportError at startup.Route Testing and Debugging
Section titled “Route Testing and Debugging”Getting All Route
Section titled “Getting All Route”You can inspect all registered routes:
# Get all routesroutes = app.get_all_routes()
for route in routes: print(f"Path: {route.raw_path}") print(f"Methods: {route.methods}") print(f"Name: {route.name}") print(f"Tags: {route.tags}") print("---")Route Matching
Section titled “Route Matching”You can test route matching:
# Test if a route matches a pathroute = Route("/users/{user_id:int}", handler=None, methods=["GET"])
# Test matchingmatch, params, allowed = route.match("/users/123", "GET")if match: print(f"Matched! Params: {params}") # {'user_id': 123} print(f"Method allowed: {allowed}") # True
# Test non-matchingmatch, params, allowed = route.match("/users/abc", "GET")print(f"Matched: {match}") # None (invalid int)
match, params, allowed = route.match("/users/123", "POST")print(f"Method allowed: {allowed}") # False
# If a route does not match the path or method, sillo will return a 404 or 405 error as appropriate.Route Debugging
Section titled “Route Debugging”Enable debug mode to see route information:
from sillo import silloApp
app = silloApp(debug=True)
# In debug mode, you'll see detailed route information# and better error messages for route matching issues# In debug mode, route matching errors will include detailed information about why a route did not match.Performance Considerations
Section titled “Performance Considerations”Route Grouping with Group
Section titled “Route Grouping with Group”The Group class in sillo provides a powerful way to organize related routes and middleware under a common path prefix. It’s particularly useful for:
- Grouping related routes under a common path prefix
- Applying middleware to a set of routes
- Mounting external ASGI applications with a path prefix
- Creating reusable route collections
Basic Group Usage
Section titled “Basic Group Usage”from sillo.routing import Group, Routefrom sillo import silloApp
app = silloApp()
# Create a group of routesuser_group = Group( path="/users", routes=[ Route(path="/", methods=["GET"], handler=list_users), Route(path="/{user_id}", methods=["GET"], handler=get_user), Route(path="/", methods=["POST"], handler=create_user), ])
app.add_route(user_group)This creates the following endpoints:
GET /users/- List all usersGET /users/{user_id}- Get a specific userPOST /users/- Create a new user
Groups with Middleware
Section titled “Groups with Middleware”You can apply middleware to all routes within a group:
async def auth_middleware(request, response, next): if not request.user.is_authenticated: return response.json({"error": "Unauthorized"}, status_code=401) return await next()user_group = Group(path="/users")api_group = Group( path="/api", middleware=[auth_middleware], routes=[ Route(path="/dashboard", methods=["GET"], handler=get_dashboard), Route(path="/profile", methods=["GET"], handler=get_profile), ])Mounting External ASGI Applications
Section titled “Mounting External ASGI Applications”Groups can be used to mount external ASGI applications:
from fastapi import FastAPIfrom sillo.routing import Group
fastapi_app = FastAPI()
@fastapi_app.get("/items")async def read_items(): return [{"item_id": "Foo"}]
# Mount the FastAPI app under /externalgroup = Group(path="/external", app=fastapi_app)app.add_route(group)Nested Groups
Section titled “Nested Groups”Groups can be nested to create hierarchical route structures:
api_v1 = Group( path="/v1", routes=[ Route(path="/status", methods=["GET"], handler=get_status) ])
auth_group = Group( path="/auth", middleware=[(auth_middleware, {}, {})], routes=[ Route(path="/login", methods=["POST"], handler=login), Route(path="/register", methods=["POST"], handler=register), ])
# Nest the auth group under the API v1 groupapi_v1.routes.append(auth_group)app.add_route(api_v1)This creates the following endpoints:
GET /v1/statusPOST /v1/auth/loginPOST /v1/auth/register
Group vs Router
Section titled “Group vs Router”While both Group and Router can be used to organize routes, they serve different purposes:
| Feature | Group | Router |
|---|---|---|
| Path prefixing | ||
| Middleware | ||
| Mount ASGI apps | ||
| Nested routing | ||
| Route methods | ||
| Standalone app |
Best Practices
Section titled “Best Practices”-
Use Groups for:
- Applying common middleware to a set of routes
- Mounting external ASGI applications
- Creating reusable route collections
-
Use Routers for:
- Organizing related routes with a common prefix
- Creating modular, self-contained route collections
- Versioning APIs
-
Naming:
- Use descriptive names that reflect the group’s purpose
- Prefix group names with their domain (e.g.,
user_auth_group,admin_api_group)
-
Middleware:
- Apply middleware at the most specific level possible
- Document any middleware applied to a group