Router API
Router Class
Section titled “Router Class”Routers allow you to organize related endpoints together and apply common configuration like prefixes, tags, and middleware.
Basic Router (Organize Your Endpoints)
Section titled “Basic Router (Organize Your Endpoints)”from zenith import Router
# WHY ROUTERS?# Problem: Putting all endpoints in main.py becomes messy after 10+ routes# Solution: Routers group related endpoints together (like users, posts, etc.)
# Create a router - like a mini-app for related endpointsrouter = Router() # No configuration needed to start
# Now define routes on the router (not the main app)@router.get("/")async def list_items(): """GET /items/ - List all items.
Note: The path "/" is relative to where this router is mounted. If mounted at /items, this becomes GET /items/ """ return {"items": []} # Empty list for now
@router.post("/")async def create_item(item: dict): """POST /items/ - Create a new item.
The 'item' parameter is automatically parsed from JSON body. """ # In real app: save to database return {"created": item} # Echo back what was created
@router.get("/{item_id}")async def get_item(item_id: int): """GET /items/{item_id} - Get specific item.
Path parameter {item_id} is extracted and converted to int. Example: GET /items/123 → item_id = 123 """ return {"item_id": item_id}
# To use this router, include it in your main app:# app.include_router(router, prefix="/items")# Now all these routes are available under /items/Router with Configuration (Production-Ready Setup)
Section titled “Router with Configuration (Production-Ready Setup)”from zenith import Routerfrom pydantic import BaseModel
# Define your models firstclass UserCreate(BaseModel): name: str email: str
# Configure router with common settings for all its routesrouter = Router( # PREFIX - Prepended to all routes in this router prefix="/api/v1/users", # So "/" becomes "/api/v1/users/" # "/{id}" becomes "/api/v1/users/{id}"
# TAGS - Group endpoints in API documentation tags=["Users"], # All routes appear under "Users" in /docs
# COMMON RESPONSES - Apply to all routes in router responses={ # These errors might happen on any endpoint 404: {"description": "User not found"}, 422: {"description": "Validation error"} # Each route inherits these + can add its own })
@router.get("/")async def list_users(): """List all users.
Full path: GET /api/v1/users/ Tagged as: "Users" in documentation Responses: 200 (success), 404, 422 (from router config) """ # Your business logic here users = await fetch_users_from_database() return {"users": users}
@router.post("/")async def create_user(user: UserCreate): """Create a new user.
Full path: POST /api/v1/users/ Body: JSON matching UserCreate model Automatic: Validation, serialization, documentation """ # user is already validated by Pydantic! new_user = await save_to_database(user) return {"user": new_user}, 201 # 201 Created status
# BENEFITS of router configuration:# 1. Don't repeat prefix on every route# 2. Automatic API documentation grouping# 3. Common error responses defined once# 4. Easy to move/rename groups of endpointsRouter Parameters
Section titled “Router Parameters”prefix
Section titled “prefix”Add a common prefix to all routes:
# Router with prefixapi_router = Router(prefix="/api/v1")user_router = Router(prefix="/users")post_router = Router(prefix="/posts")
@user_router.get("/") # Becomes /users/async def list_users(): return {"users": []}
@post_router.get("/") # Becomes /posts/async def list_posts(): return {"posts": []}
# Include in main routerapi_router.include_router(user_router)api_router.include_router(post_router)
# Include in app (final paths: /api/v1/users/, /api/v1/posts/)app.include_router(api_router)Group endpoints in OpenAPI documentation:
# Separate routers by featureuser_router = Router(prefix="/users", tags=["Users"])auth_router = Router(prefix="/auth", tags=["Authentication"])admin_router = Router(prefix="/admin", tags=["Administration"])
@user_router.get("/")async def list_users(): return {"users": []}
@auth_router.post("/login")async def login(credentials: LoginRequest): return {"token": "..."}dependencies (Apply Auth/Logic to All Routes)
Section titled “dependencies (Apply Auth/Logic to All Routes)”from zenith import Router, Inject, Dependsfrom zenith.auth import get_current_user
# PROBLEM: You have 20 endpoints that all need authentication# BAD: Adding auth dependency to every single route# GOOD: Apply auth once at router level!
# Create router where ALL routes require authenticationprotected_router = Router( prefix="/protected",
# DEPENDENCIES run before every route in this router dependencies=[ Inject(get_current_user) # Validates JWT token ] # Can have multiple dependencies: # dependencies=[ # Inject(get_current_user), # Auth check # Inject(check_subscription), # Payment check # Inject(log_request) # Audit logging # ])
@protected_router.get("/profile")async def get_profile( # Note: No auth dependency here! # Router already handled authentication current_user = Depends(get_current_user) # Get the authenticated user): """Get user profile.
Authentication flow: 1. Request comes to /protected/profile 2. Router runs get_current_user dependency 3. If auth fails: Returns 401 Unauthorized 4. If auth succeeds: Runs this function 5. current_user is available here """ return { "profile": { "id": current_user.id, "email": current_user.email } }
@protected_router.post("/settings")async def update_settings( settings: UserSettings, current_user = Depends(get_current_user)): """Update user settings.
This is also protected by router-level auth. No need to repeat auth logic! """ # Update settings for authenticated user await update_user_settings(current_user.id, settings) return {"updated": True}
# UNPROTECTED router for comparisonpublic_router = Router(prefix="/public")# No dependencies = no authentication required
@public_router.get("/info")async def public_info(): """This endpoint is publicly accessible.""" return {"message": "This is public information"}responses
Section titled “responses”Define common response schemas:
common_responses = { 400: {"description": "Bad Request", "model": ErrorResponse}, 401: {"description": "Unauthorized", "model": ErrorResponse}, 404: {"description": "Not Found", "model": ErrorResponse}, 500: {"description": "Internal Server Error", "model": ErrorResponse}}
router = Router( prefix="/users", tags=["Users"], responses=common_responses)
@router.get("/{user_id}", responses={ 200: {"model": UserResponse}})async def get_user(user_id: int): # Inherits common responses + adds 200 response return {"user": "..."}Route Methods
Section titled “Route Methods”HTTP Methods
Section titled “HTTP Methods”router = Router(prefix="/items")
@router.get("/") # GET /items/async def list_items(): return {"items": []}
@router.post("/") # POST /items/async def create_item(item: ItemCreate): return {"created": item}
@router.get("/{item_id}") # GET /items/{item_id}async def get_item(item_id: int): return {"item_id": item_id}
@router.put("/{item_id}") # PUT /items/{item_id}async def update_item(item_id: int, item: ItemUpdate): return {"updated": item_id}
@router.patch("/{item_id}") # PATCH /items/{item_id}async def partial_update(item_id: int, item: ItemPartial): return {"patched": item_id}
@router.delete("/{item_id}") # DELETE /items/{item_id}async def delete_item(item_id: int): return {"deleted": item_id}
@router.head("/{item_id}") # HEAD /items/{item_id}async def check_item_exists(item_id: int): return # Just headers, no body
@router.options("/") # OPTIONS /items/async def item_options(): return {"methods": ["GET", "POST"]}Route Decorators with Parameters
Section titled “Route Decorators with Parameters”@router.get( "/{item_id}", response_model=ItemResponse, status_code=200, summary="Get item by ID", description="Retrieve a single item by its unique identifier", response_description="The requested item", tags=["Items", "Retrieval"], deprecated=False, operation_id="getItemById", responses={ 404: {"description": "Item not found"} })async def get_item(item_id: int = Path(..., description="Item ID")): return {"item_id": item_id}Including Routers
Section titled “Including Routers”Basic Inclusion (Connecting Routers to Your App)
Section titled “Basic Inclusion (Connecting Routers to Your App)”from zenith import Zenith, Router
# Step 1: Create your main applicationapp = Zenith()
# Step 2: Create routers for different features# Think of routers like folders organizing your code
# User-related endpointsusers_router = Router(prefix="/users")# Routes: /users/, /users/{id}, /users/me, etc.
# Item-related endpointsitems_router = Router(prefix="/items")# Routes: /items/, /items/{id}, /items/search, etc.
# Auth endpointsauth_router = Router(prefix="/auth")# Routes: /auth/login, /auth/register, /auth/logout
# Step 3: Define routes on each router (usually in separate files)@users_router.get("/")async def list_users(): return {"users": []}
@items_router.get("/")async def list_items(): return {"items": []}
@auth_router.post("/login")async def login(credentials: LoginRequest): return {"token": "..."}
# Step 4: Connect routers to main appapp.include_router(users_router) # Adds all /users/* routesapp.include_router(items_router) # Adds all /items/* routesapp.include_router(auth_router) # Adds all /auth/* routes
# That's it! Your app now has:# GET /users/# GET /items/# POST /auth/login
# WHY THIS PATTERN?# 1. Keeps main.py small and clean# 2. Easy to find endpoints (users in users.py, etc.)# 3. Can disable features by commenting one line# 4. Teams can work on different routers without conflictsNested Router Structure (API Versioning Pattern)
Section titled “Nested Router Structure (API Versioning Pattern)”# PROBLEM: Your API evolves but you need to support old clients# SOLUTION: API versioning with nested routers
# Level 1: Version routersapi_v1 = Router( prefix="/api/v1", tags=["API v1"] # Groups in documentation)
api_v2 = Router( prefix="/api/v2", tags=["API v2"] # Separate section in docs)
# Level 2: Feature routers for each version
# ===== VERSION 1 ROUTES =====users_v1 = Router(prefix="/users")posts_v1 = Router(prefix="/posts")
@users_v1.get("/")async def list_users_v1(): """V1: Returns simple user list.""" return {"users": ["Alice", "Bob"]} # Just names
@posts_v1.get("/")async def list_posts_v1(): return {"posts": [...]} # V1 format
# Connect V1 features to V1 APIapi_v1.include_router(users_v1) # Becomes /api/v1/usersapi_v1.include_router(posts_v1) # Becomes /api/v1/posts
# ===== VERSION 2 ROUTES =====users_v2 = Router(prefix="/users")posts_v2 = Router(prefix="/posts")
@users_v2.get("/")async def list_users_v2(): """V2: Returns detailed user objects.""" return { "users": [ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"} ], "pagination": {"page": 1, "total": 2} # V2 adds pagination }
@posts_v2.get("/")async def list_posts_v2(): return {"posts": [...], "meta": {...}} # V2 format
# Connect V2 features to V2 APIapi_v2.include_router(users_v2) # Becomes /api/v2/usersapi_v2.include_router(posts_v2) # Becomes /api/v2/posts
# Level 3: Connect version routers to main appapp.include_router(api_v1) # All V1 routesapp.include_router(api_v2) # All V2 routes
# FINAL ROUTE STRUCTURE:# /api/v1/users/ → Returns ["Alice", "Bob"]# /api/v2/users/ → Returns [{id, name, email}, ...] with pagination# /api/v1/posts/ → Old format# /api/v2/posts/ → New format with metadata
# BENEFITS:# 1. Old clients keep working with v1# 2. New clients get enhanced v2 features# 3. Can deprecate v1 after migration period# 4. Clear separation in code and docsRouter with Override Parameters (Reuse with Modifications)
Section titled “Router with Override Parameters (Reuse with Modifications)”# SCENARIO: You have a router but need to mount it differently# Example: Same routes for internal and external APIs
# Original router definition (e.g., in routes/users.py)users_router = Router( prefix="/users", # Base prefix tags=["Users"] # Base tags)
@users_router.get("/")async def list_users(): return {"users": [...]}
@users_router.get("/{user_id}")async def get_user(user_id: int): return {"user": {...}}
# MOUNT 1: Public API with API key requirementapp.include_router( users_router,
# OVERRIDE PREFIX - Prepends to router's prefix prefix="/api/v1", # Router has: /users # This adds: /api/v1 # Result: /api/v1/users
# ADD TAGS - Combines with router's tags tags=["Public API"], # Router has: ["Users"] # This adds: ["Public API"] # Result: ["Public API", "Users"]
# ADD DEPENDENCIES - Extra requirements dependencies=[Inject(require_api_key)] # Now all routes need valid API key)
# MOUNT 2: Internal API with authenticationapp.include_router( users_router, # Same router!
prefix="/internal", # Different prefix # Result: /internal/users
tags=["Internal"], # Different tags
dependencies=[Inject(require_employee_auth)] # Different auth requirement)
# NOW YOUR APP HAS:# Public API:# GET /api/v1/users/ (requires API key)# GET /api/v1/users/{id} (requires API key)## Internal API:# GET /internal/users/ (requires employee auth)# GET /internal/users/{id} (requires employee auth)## Same code, different access patterns!
# USE CASES:# 1. Public vs internal APIs# 2. Free vs premium endpoints# 3. Different auth for different clients# 4. A/B testing different API designsRouter Middleware
Section titled “Router Middleware”Router-Specific Middleware
Section titled “Router-Specific Middleware”from zenith.middleware import RateLimitMiddleware
# Create router with middlewareapi_router = Router( prefix="/api", middleware=[ RateLimitMiddleware({"requests_per_minute": 100}) ])
@api_router.get("/data")async def get_data(): # Rate limiting applied only to this router's routes return {"data": "..."}
# Include in appapp.include_router(api_router)Conditional Middleware
Section titled “Conditional Middleware”import osfrom zenith.middleware import SecurityHeadersMiddleware, CORSMiddleware
# Different middleware for different environmentsif os.getenv("ENVIRONMENT") == "production": middleware = [ SecurityHeadersMiddleware({"force_https": True}), RateLimitMiddleware({"requests_per_minute": 60}) ]else: middleware = [ CORSMiddleware({"allow_origins": ["*"]}) ]
api_router = Router( prefix="/api", middleware=middleware)Router Events
Section titled “Router Events”Router Startup/Shutdown
Section titled “Router Startup/Shutdown”from zenith import Router
router = Router(prefix="/data")
@router.on_event("startup")async def startup_event(): """Initialize router resources.""" print("Data router starting up") # Initialize connections, caches, etc.
@router.on_event("shutdown")async def shutdown_event(): """Cleanup router resources.""" print("Data router shutting down") # Close connections, cleanup resources
@router.get("/")async def get_data(): return {"data": "..."}
# Events are executed when router is includedapp.include_router(router)Multiple Event Handlers
Section titled “Multiple Event Handlers”router = Router(prefix="/services")
@router.on_event("startup")async def init_database(): """Initialize database connection.""" print("Initializing database...")
@router.on_event("startup")async def init_cache(): """Initialize cache.""" print("Initializing cache...")
@router.on_event("startup")async def init_external_services(): """Connect to external services.""" print("Connecting to external services...")
# All startup handlers will be executed in orderRoute Organization Patterns
Section titled “Route Organization Patterns”Feature-Based Organization
Section titled “Feature-Based Organization”from zenith import Routerfrom app.services.users import UserService
router = Router(prefix="/users", tags=["Users"])
@router.get("/")async def list_users(users: UserService = Inject()): return await users.list_users()
@router.post("/")async def create_user( user_data: UserCreate, users: UserService = Inject()): return await users.create_user(user_data)
# app/routes/posts.pyfrom zenith import Routerfrom app.services.posts import PostService
router = Router(prefix="/posts", tags=["Posts"])
@router.get("/")async def list_posts(posts: PostService = Inject()): return await posts.list_posts()
# app/main.pyfrom app.routes import users, posts
app.include_router(users.router)app.include_router(posts.router)Versioned API Structure
Section titled “Versioned API Structure”from zenith import Routerfrom . import users, posts, auth
router = Router(prefix="/v1", tags=["API v1"])
router.include_router(auth.router)router.include_router(users.router)router.include_router(posts.router)
# app/routes/v2/__init__.pyfrom zenith import Routerfrom . import users, posts, auth
router = Router(prefix="/v2", tags=["API v2"])
router.include_router(auth.router)router.include_router(users.router)router.include_router(posts.router)
# app/main.pyfrom app.routes import v1, v2
app.include_router(v1.router, prefix="/api")app.include_router(v2.router, prefix="/api")Domain-Driven Structure
Section titled “Domain-Driven Structure”from zenith import Routerfrom .service import UserServicefrom .models import User, UserCreate, UserUpdate
router = Router(prefix="/users", tags=["Users"])
@router.get("/", response_model=List[User])async def list_users(users: UserService = Inject()): return await users.list_users()
# app/domains/order/routes.pyfrom zenith import Routerfrom .service import OrderServicefrom .models import Order, OrderCreate
router = Router(prefix="/orders", tags=["Orders"])
@router.post("/", response_model=Order)async def create_order( order_data: OrderCreate, orders: OrderService = Inject()): return await orders.create_order(order_data)
# app/main.pyfrom app.domains.user.routes import router as user_routerfrom app.domains.order.routes import router as order_router
app.include_router(user_router)app.include_router(order_router)Router Testing
Section titled “Router Testing”from zenith.testing import TestClientfrom zenith import Zenith, Routerimport pytest
# Create test routertest_router = Router(prefix="/test")
@test_router.get("/")async def test_endpoint(): return {"message": "test"}
@test_router.post("/items")async def create_test_item(item: dict): return {"created": item}
# Test router in isolation@pytest.mark.asyncioasync def test_router(): app = Zenith() app.include_router(test_router)
async with TestClient(app) as client: # Test GET response = await client.get("/test/") assert response.status_code == 200 assert response.json() == {"message": "test"}
# Test POST response = await client.post("/test/items", json={"name": "test"}) assert response.status_code == 200 assert response.json() == {"created": {"name": "test"}}Advanced Router Features
Section titled “Advanced Router Features”Custom Route Classes
Section titled “Custom Route Classes”from zenith.routing import APIRoutefrom typing import Callable
class TimingRoute(APIRoute): def get_route_handler(self) -> Callable: original_route_handler = super().get_route_handler()
async def custom_route_handler(request): import time start_time = time.time() response = await original_route_handler(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response
return custom_route_handler
# Use custom route classrouter = Router( prefix="/timed", route_class=TimingRoute)
@router.get("/")async def timed_endpoint(): return {"message": "This response includes timing headers"}Router with Custom Dependency Resolution
Section titled “Router with Custom Dependency Resolution”class DatabaseRouter(Router): def __init__(self, db_url: str, *args, **kwargs): super().__init__(*args, **kwargs) self.db_url = db_url
def get_database_session(self): # Custom dependency for this router return create_session(self.db_url)
# Use custom routerdb_router = DatabaseRouter( db_url="postgresql://localhost/special_db", prefix="/special")
@db_router.get("/data")async def get_special_data(): # Uses the router's custom database session return {"data": "from special database"}Router Best Practices
Section titled “Router Best Practices”- Group by Feature - Organize routes by domain/feature, not by HTTP method
- Use Prefixes - Apply consistent URL prefixes for better organization
- Tag Appropriately - Use tags to group related endpoints in documentation
- Apply Common Dependencies - Use router-level dependencies for shared requirements
- Version Your APIs - Use separate routers for different API versions
- Keep Routers Focused - Each router should have a single responsibility
- Test Routers in Isolation - Test router functionality independently
Routers are essential for building maintainable, well-organized APIs in Zenith. They provide the structure and flexibility needed for complex applications while maintaining clean separation of concerns.