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 a custom route middleware in Lila is exceptionally simple. A route middleware is a standard Python decorator that intercepts the request, runs custom logic, and then either returns an early response (like a redirect or an error code) or lets the execution continue by calling and returning the output of the next function.
from functools import wraps
from lila.core.request import Request
from lila.core.responses import JSONResponse
def require_premium_role(func):
"""
Route middleware to enforce that the user has a premium subscription.
"""
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
# Read a session variable or header
is_premium = request.session.get("is_premium", False)
if not is_premium:
# Short-circuit the request and return an early response
return JSONResponse({"error": "Forbidden", "message": "Premium role required"}, status_code=403)
# Continue with the execution of the next middleware or route handler
return await func(request, *args, **kwargs)
return wrapper
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.
@router.route(path='/api/example', methods=['POST'], model=ExampleModel)
async def login(request: Request):
# Validation is automatic!
# 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, all routes are cached for 30 seconds, but you can customize this
globally or per route.
# Global configuration (default is 30s)
# By default, it isolates cache by user checking "session", "auth", "auth_admin" cookies.
# You can customize the cookies to check using cache_cookie_keys.
router = Router(default_cache_ttl=60, cache_cookie_keys=["my_custom_session"])
# Disable caching for a specific route
@router.get("/live-data", cache_ttl=0)
async def live(request: Request):
return JSONResponse({"time": time.time()})
# Set a custom TTL for a specific route
@router.get("/slow-data", cache_ttl=300) # 5 minutes
async def slow(request: Request):
return JSONResponse({"data": "long-lived"})
Note: Caching is automatically disabled when DEBUG = True in your configuration to ensure you always see the latest
changes during development.
SPA Performance: Client-Side Caching & Prefetching
To deliver a native application experience with near-instantaneous (0ms) page transitions, Lila's SPA navigation engine (spa.js) combines SWR (Stale-While-Revalidate) caching and smart link prefetching.
1. Client-Side SWR Caching
When a user clicks an internal link, the SPA engine checks its local client-side memory cache:
- Stale Cache Hit: If cached data is available, Lila immediately renders the cached page, eliminating loading indicators.
- Silent Revalidation: In the background, Lila requests the latest version from the server. If the server content has changed, Lila updates the DOM and the cache seamlessly without interrupting the user.
- Cache Miss: If the page is not in the cache, the loader makes a standard background request and renders the page upon completion.
2. Smart Hover & Touch Prefetching
Lila anticipates user navigation by prefetching eligible links ahead of time:
- Desktop Hover Debounce: On desktop, if the cursor hovers over an eligible link for more than 80ms, Lila automatically prefetches the page JSON payload in the background. The 80ms delay prevents redundant requests during fast mouse movements.
- Mobile TouchStart Acceleration: On mobile/touch devices, a
touchstartevent on a link initiates an immediate prefetch, taking advantage of the typical 100-300ms delay before a click event fires.
3. Bandwidth and Connection Safeguards
To respect user bandwidth and reduce server load, Lila implements several intelligent safeguards:
- Network Detection: Prefetching is automatically disabled if the user is on a slow connection (2G, 3G) or if
saveDatamode is enabled in the browser. - Route Filtering: External links, downloads (using the
downloadattribute), links withtarget="_blank", and routes marked withdata-no-spaare ignored by the SPA engine. - Granular Prefetch Control: You can disable prefetching on specific links by adding the
data-no-prefetchattribute:<a href="/expensive-page" data-no-prefetch>Not Prefetched</a>
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})