Organize sillo routes into modular Routers and Groups — prefixes, nesting, mounting sub-applications (including external ASGI apps), and per-router middleware and dependencies.
Routers & Sub-Applications
Section titled “Routers & Sub-Applications”As an app grows, a single flat list of @app.get(...) decorators becomes hard to navigate. sillo lets you group routes into Router objects with their own path prefix, mount them under the main app, and nest them arbitrarily. You can also mount an entire sub-application — another silloApp, or any ASGI app such as a FastAPI service — under a path using a Group.
The mental model:
- A
Routeris a collection of routes plus a prefix. It is not itself an app; you mount it onto an app (or another router). - A
Groupis a mount point: it places either a sub-app or a set of routes behind a single path prefix, optionally wrapped in middleware.
Both flatten into the app’s route table at startup, so nesting adds no per-request overhead.
The smallest useful form
Section titled “The smallest useful form”from sillo import silloAppfrom sillo.routing import Router
app = silloApp()
v1 = Router(prefix="/v1")
@v1.get("/users")async def list_users(request, response): return response.json({"users": []})
@v1.get("/users/{user_id}")async def get_user(request, response, user_id): return response.json({"user_id": user_id})
app.mount_router(v1)app.mount_router(v1) folds the router’s routes into the app under /v1, so the handlers answer GET /v1/users and GET /v1/users/{user_id}.
Router options
Section titled “Router options”Router(...) accepts more than a prefix:
v1 = Router( prefix="/v1", tags=["v1"], # OpenAPI tag applied to all routes dependencies=[...], # Depends applied to every route in the router middleware=[...], # router-level middleware (see below) name="api-v1", # name for the router group)tags— groups the router’s endpoints under a tag in generated OpenAPI docs.dependencies— a list ofDepend(...)objects resolved for every route in the router (e.g. a shared auth dependency).middleware— functions applied to requests matching this router’s routes.name— an identifier for the router (mostly for referencing in tooling/URL generation within the router tree).
Nesting routers
Section titled “Nesting routers”A router can mount another router, building a deep prefix tree:
app = silloApp()
v1 = Router(prefix="/v1")users = Router(prefix="/users")
@users.get("/")async def users_index(request, response): return response.text("User root")
@users.get("/{id}")async def users_detail(request, response, id): return response.json({"user": id})
# nest: /v1/users/*v1.mount_router(users)# mount the tree onto the app: /v1/users/*app.mount_router(v1)Final paths: /v1/users/ and /v1/users/{id}. You can nest as deeply as you like — v1.mount_router(users), users.mount_router(posts), and so on. sillo resolves the full prefix by walking the tree at startup.
Per-router middleware and dependencies
Section titled “Per-router middleware and dependencies”Middleware and dependencies declared on a router run only for routes under that router. This is how you scope “require auth for everything under /admin” without touching each handler:
from sillo import silloApp, Dependfrom sillo.routing import Router
app = silloApp()
admin = Router(prefix="/admin", tags=["admin"])
def require_staff(request): if not request.headers.get("X-Staff"): from sillo.exceptions import HTTPException raise HTTPException(403, "staff only") return True
@admin.get("/dashboard")async def dashboard(request, response, _staff=Depend(require_staff)): return response.text("secret dashboard")
app.mount_router(admin)Here require_staff resolves for every route registered on admin. (You can also pass dependencies=[Depend(require_staff)] to the Router constructor to apply it uniformly.)
Groups: mounting a sub-application
Section titled “Groups: mounting a sub-application”When the thing you want to mount is itself an app — a separate silloApp, or any ASGI app — use Group. A Group takes either app= (an ASGI app) or routes= (a list of Route objects), plus a path prefix.
Mounting another silloApp
Section titled “Mounting another silloApp”from sillo import silloAppfrom sillo.routing import Group
main_app = silloApp()admin_app = silloApp()
@admin_app.get("/dashboard")async def dashboard(request, response): return response.text("Welcome to the admin panel")
admin_group = Group(path="/admin", app=admin_app)main_app.add_route(admin_group)Now /admin/dashboard is served by admin_app. The sub-app keeps its own routes, handlers, and (if you add them) its own middleware — useful when different teams own different parts of a system.
Mounting a list of routes
Section titled “Mounting a list of routes”from sillo.routing import Router, Group, Routefrom sillo import silloApp
users = Router()
async def list_users(request, response): return response.json(["John", "Jane"])
async def get_user(request, response, id): return response.json({"user": id})
group = Group( path="/users", routes=[ Route(path="/", methods=["GET"], handler=list_users), Route(path="/{id}", methods=["GET"], handler=get_user), ],)app = silloApp()app.add_route(group)This answers /users and /users/{id}. Group with routes= is essentially a prefix wrapper around a set of Route objects; Group with app= mounts a whole app.
Mounting external ASGI apps
Section titled “Mounting external ASGI apps”Because a Group accepts any ASGI app, you can mount a FastAPI (or Starlette, Quart, …) service under a path without rewriting it:
from sillo import silloAppfrom sillo.routing import Groupfrom fastapi import FastAPI
app = silloApp()fast_app = FastAPI()
@fast_app.get("/ping")def ping(): return {"ping": "pong"}
fast_group = Group(path="/service2", app=fast_app)app.add_route(fast_group)Requests to /service2/ping are delegated to fast_app, whose own routing and middleware run normally. This is the migration-friendly escape hatch: keep a legacy service running while new routes live in sillo.
Route names and URL generation
Section titled “Route names and URL generation”Routes (and routers) accept a name= used with url_for to build URLs without hard-coding paths:
@v1.get("/users/{user_id}", name="get-user")async def get_user(request, response, user_id): return response.json({"user_id": user_id})
@app.get("/home")async def home(request, response): url = request.url_for("get-user", user_id=42) # -> /v1/users/42 return response.json({"link": str(url)})When a route lives under a router prefix, url_for includes that prefix automatically. Name routes once and generate links everywhere.
Putting it together: a modular app
Section titled “Putting it together: a modular app”from sillo import silloApp, Dependfrom sillo.routing import Router, Group
app = silloApp()
# public API v1api = Router(prefix="/api/v1", tags=["api"])
@api.get("/health")async def health(request, response): return response.json({"status": "ok"})
# admin sub-appadmin = silloApp()
@admin.get("/stats")async def stats(request, response): return response.json({"requests": 0})
# composeapp.mount_router(api)app.add_route(Group(path="/admin", app=admin))This gives you /api/v1/health and /admin/stats from two independently-defined pieces.
Works with
Section titled “Works with”- Routing — path syntax, converters,
name=, and all route options - Handlers — the handler contract used inside routers
- Middleware — router-scoped and global middleware
- Handlers — define function handlers for router endpoints
- Dependency Injection — router-level
dependencies=
Related topics
Section titled “Related topics”- Startup & Shutdown — run setup when the composed app boots
- Error Handling — exception handlers registered per app vs. globally
- Events — router-level event hooks