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).

routes/api.py

# 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.

Receiving GET Parameters

@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:

Receiving GET Query Parameters
 
@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})
            
Automatic Sanitization
 
# 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"})
            
Basic Imports

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.

Middleware Validation

@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.

Applying Middlewares to Router

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.

Creating a Custom Middleware

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.

Validation with Pydantic

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.

Documentation Generation

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.

Route Import

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.

Caching Configuration

# 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:

2. Smart Hover & Touch Prefetching

Lila anticipates user navigation by prefetching eligible links ahead of time:

3. Bandwidth and Connection Safeguards

To respect user bandwidth and reduce server load, Lila implements several intelligent safeguards:

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.

routes/api_v2.py

# 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.

routes/v1_api_example.py

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:

Documentation is automatically generated at /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.

Manual Parsing Example

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})