Routes
Routes are the access points to the application. In Lila, routes are defined in the
routes directory (by default, but you can place them
wherever you want) and
imported into main.py for use. Routes can be configured
to handle HTTP requests, API
methods, and more.
In addition to JSONResponse you can use HTMLResponse, RedirectResponse and PlainTextResponse, or StreamingResponse, to transmit data in real-time (useful for streaming video/audio or large responses).
# Import from lila.core JSONResponse.
from lila.core.responses import JSONResponse
# Manages routes for API endpoints.
from lila.core.routing import Router
# Initializes the router instance to handle API app.routes.
router = Router()
# Defines a simple API route that supports the GET method.
@router.route(path='/api', methods=['GET'])
async def api(request: Request):
"""Api function"""
# English: Returns a simple JSON response for API verification.
return JSONResponse({'api': True})
Get URL Parameters
In this function, we receive a parameter via the URL using {param}.
If the parameter is not provided, it defaults to 'default'.
The response is a JSON containing the received value.
@router.route(path='/url_with_param/{param}', methods=['GET'])
async def param(request: Request):
param = request.path_params.get('param', 'default')
return JSONResponse({"received_param": param})
Or you can also do it like this:
@router.route(path='/url_with_query', methods=['GET','POST'])
async def query_param(request: Request):
query_param = request.query_params.get('query_param', 'default_value')
return JSONResponse({"received_param": query_param})
# All POST/PUT/PATCH routes automatically sanitize the body
# Query parameters are also checked for XSS patterns
@router.post('/secure-route', model=MyModel)
async def secure_handler(request: Request):
# request.state.data is already sanitized
data = request.state.data
return JSONResponse({"status": "safe"})
from lila.core.responses import JSONResponse # Simplifies sending JSON responses.
from lila.core.routing import Router # Manages API app.routes.
from lila.core.request import Request # Handles HTTP requests in the application.
from pydantic import EmailStr, BaseModel # Validates and parses data models for input validation.
from lila.core.middleware import validate_token
router = Router()
️ Middleware and Decorators Usage
Middlewares allow intercepting requests before they reach the main logic of the API.
In this example, we use @validate_token to validate a
JWT token in the
request header.
@router.route(path='/api/token', methods=['GET','POST'])
@validate_token # Middleware to validate JWT token.
async def api_token(request: Request):
return JSONResponse({'api': True})
️ Router-Level Middleware Inheritance
You can apply one or more middlewares directly to a Router instance
during initialization. When you do this, every route registered on that router will automatically
inherit and execute those middlewares in the exact sequence they are defined (left-to-right).
This pattern is perfect for creating dedicated authentication groups, admin-only sections, rate-limited zones, or custom logging areas without having to manually apply decorators to every single endpoint.
from lila.core.routing import Router
from lila.core.middleware import login_required, validate_token
from lila.core.request import Request
from lila.core.responses import JSONResponse
# All routes registered on this router will require the login session first,
# then they will validate the JWT token.
auth_router = Router(prefix="api/v1", middlewares=[login_required, validate_token])
# This route automatically inherits and executes both middlewares first!
@auth_router.get("/profile")
async def get_profile(request: Request):
# If the execution reaches here, the user is guaranteed to be authenticated.
return JSONResponse({"profile": "User Profile Data"})
️ Creating Custom Route Middlewares
Creating custom route middlewares in Lila is clean and effortless thanks to the built-in create_decorator helper from lila.core.middleware.
Instead of writing boilerplate decorator wrappers manually with functools.wraps, you simply define an async logic function decorated with @create_decorator.
If your logic function returns a response (like a JSONResponse or redirect), Lila breaks the chain early and returns that response. If it returns True or None, execution continues to the route handler. It automatically supports both unparenthesized usage (@check_admin) and parameterized usage (@check_admin(role="superadmin")).
from lila.core.middleware import create_decorator
from lila.core.request import Request
from lila.core.responses import JSONResponse
@create_decorator
async def check_admin(request: Request, role="admin"):
# If the logic returns a response, it breaks the chain and returns it.
# If it returns True or None, execution continues to the route handler.
user = getattr(request.state, "user", {})
if user.get("role") != role:
return JSONResponse({"error": "Unauthorized", "message": f"{role} role required"}, status_code=403)
return True
# Usage in routes:
# @check_admin -> uses default role="admin"
# @check_admin(role="superadmin") -> uses custom role
@router.get("/admin-panel")
@check_admin
async def admin_panel(request: Request):
return JSONResponse({"status": "welcome admin"})
@router.get("/superadmin-panel")
@check_admin(role="superadmin")
async def superadmin_panel(request: Request):
return JSONResponse({"status": "welcome superadmin"})
Data Validation with Pydantic
Pydantic allows defining data models that automatically validate user input.
Additionally, specifying a model in the route generates automatic documentation in
/docs.
from pydantic import EmailStr, BaseModel
from lila.core.responses import JSONResponse
class ExampleModel(BaseModel):
email: EmailStr # Ensures a valid email.
password: str # String for password.
# Method 1: Inject via Model Parameter
@router.post('/api/example')
async def login(request: Request, body: ExampleModel):
# Validation is completely automatic!
# Validated body is directly passed as an argument.
return JSONResponse({"email": body.email, "status": "validated"})
# Method 2: Inject via route decorator parameter
@router.route(path='/api/example2', methods=['POST'], model=ExampleModel)
async def login_legacy(request: Request):
# Validated data is injected into request.state.data
user_data = request.state.data
return JSONResponse({"email": user_data.email, "status": "validated"})
Automatic Documentation Generation
Thanks to the integration with Pydantic, the API documentation is generated
automatically
and is accessible from /docs. You can also generate an
OpenAPI JSON file for external tools.
router.swagger_ui() # Enables Swagger UI for API documentation.
router.openapi_json() # Generates OpenAPI JSON for external tools.
Importing Routes in main.py
To use the routes defined in the router, you need to obtain them with
router.get_routes() and import them into main.py.
routes = router.get_routes() # Retrieves all defined app.routes.
Performance: Route Caching
Lila includes a built-in caching system for GET requests to improve
performance. By default, route caching is disabled (cache_ttl = 0) to ensure real-time dynamic responses. You can easily activate caching globally per router or on specific routes.
# Global router caching (e.g., cache all routes in this router for 60s)
# Isolates cache by user using session/auth cookies automatically.
router = Router(default_cache_ttl=60, cache_cookie_keys=["session", "auth"])
# Route-specific caching (e.g., cache heavy endpoint for 5 minutes)
@router.get("/heavy-catalog", cache_ttl=300) # 5 minutes
async def catalog(request: Request):
return JSONResponse({"data": "catalog items"})
# Standard route without cache_ttl remains uncached (real-time execution)
@router.get("/realtime-feed")
async def feed(request: Request):
return JSONResponse({"status": "live"})
Note: Caching is automatically disabled when DEBUG = True in your configuration to ensure you always see the latest
changes during development.
Performance Win: Combining route caching with Lila's Single-Flight query deduplication allows handling huge traffic spikes with minimal resources. In local stress tests, sending 50,000 concurrent requests yielded 100% successful responses with a max CPU usage under 52% and RAM staying under 132 MB!
Route Prefixes and HTTP Method Shortcuts
Routes can now have a prefix, which allows grouping
multiple endpoints under a common path.
Additionally, you can define routes directly with HTTP methods as functions: get,
post, put, delete.
# English: Initialize the router instance for managing API routes.
router = Router(prefix="v1/api")
# English: Define a simple API route that supports GET method.
@router.get("/")
async def api(request: Request):
"""Api function""" # use doc for description http://127.0.0.1:8000/openapi.json and http://127.0.0.1:8000/docs
# English: Returns a simple JSON response for API verification.
return JSONResponse({"api": True})
# Español: Define una ruta de API que soporta los métodos GET y POST.
@router.route(path="/token", methods=["GET", "POST"])
# English: Middleware to validate the JWT Token.
@validate_token
async def api_token(request: Request):
"""Api Token function""" # use doc for description http://127.0.0.1:8000/openapi.json and http://127.0.0.1:8000/docs
print(get_user_by_token(request=request))
return JSONResponse({"api": True})
Using prefixes helps organize your API under a common version path (e.g., v1/api),
while method-specific decorators (@router.get, @router.post, etc.)
make route definitions cleaner and more intuitive.
HTTP Method Examples with Prefix and Pydantic Models
Routes now support the HTTP methods get, post,
put, and delete
directly.
You can also attach Pydantic models for automatic input validation and documentation
generation.
from lila.core.responses import JSONResponse
from lila.core.routing import Router
from lila.core.request import Request
from pydantic import BaseModel
router = Router(prefix="v1/api")
# ---------------- GET Example ----------------
@router.get("/items/{item_id}")
async def get_item(request: Request):
"""Fetch an item by ID."""
item_id = request.path_params.get("item_id")
return JSONResponse({"item_id": item_id, "status": "fetched"})
# ---------------- POST Example ----------------
class CreateItemModel(BaseModel):
name: str
description: str
@router.post("/items", model=CreateItemModel)
async def create_item(request: Request):
"""Create a new item."""
# Data is already validated and available in request.state.data
input_data = request.state.data
return JSONResponse({"name": input_data.name, "description": input_data.description})
# ---------------- PUT Example ----------------
class UpdateItemModel(BaseModel):
name: str
description: str
@router.put("/items/{item_id}", model=UpdateItemModel)
async def update_item(request: Request):
"""Update an existing item by ID."""
item_id = request.path_params.get("item_id")
# Data is already validated and available in request.state.data
input_data = request.state.data
return JSONResponse({"item_id": item_id, "updated_name": input_data.name})
# ---------------- DELETE Example ----------------
@router.delete("/items/{item_id}")
async def delete_item(request: Request):
"""Delete an item by ID."""
item_id = request.path_params.get("item_id")
return JSONResponse({"item_id": item_id, "status": "deleted"})
These examples demonstrate how to:
- Use a
prefixfor versioned API endpoints. - Use method-specific decorators like
@router.get,@router.post, etc. - Automatically validate input and generate documentation using Pydantic models.
- Receive URL parameters and JSON bodies.
/docs and /openapi.json.
️ Manual Validation with RequestParser
While assigning a model=ExampleModel to the route automatically
validates incoming data and injects it into request.state.data,
sometimes you may need more granular control over parsing the request body or query parameters manually.
For these edge cases, Lila provides a built-in RequestParser in the
lila.core.controller module. It safely parses and validates the data
against a Pydantic model and returns a standardized dictionary with the results.
from lila.core.controller import RequestParser
from pydantic import BaseModel
from lila.core.responses import JSONResponse
class MyCustomModel(BaseModel):
username: str
age: int
parser = RequestParser()
@router.post("/manual-parse")
async def manual_parse_endpoint(request: Request):
# Parses the JSON body and validates it against MyCustomModel
result = await parser.parse_body(request, MyCustomModel)
if not result["success"]:
# Returns standard validation errors
return JSONResponse({"error": True, "details": result["errors"]}, status_code=400)
# Access the validated Pydantic model
validated_data = result["data"]
return JSONResponse({"status": "parsed", "user": validated_data.username})