Middleware¶
Learn how middleware works in Aksara and how to use the built-in middleware.
What is Middleware?¶
Middleware is code that runs before and after every request to your application.
Think of it like security checkpoints at an airport:
Request → [Middleware 1] → [Middleware 2] → [Your Code] → [Middleware 2] → [Middleware 1] → Response
Each middleware can: - Inspect the request before it reaches your code - Modify the request or response - Block requests that shouldn't proceed - Add information to the request (like user identity)
Why Use Middleware?¶
Common use cases:
| Use Case | What It Does |
|---|---|
| Authentication | Check if user is logged in |
| Request ID | Add unique ID to track requests |
| Logging | Record all requests and responses |
| CORS | Allow cross-origin requests |
| Multi-tenancy | Identify which tenant made the request |
| Rate Limiting | Prevent too many requests |
Built-in Middleware¶
Aksara includes these middleware out of the box:
| Middleware | Purpose |
|---|---|
| Request ID | Adds a unique ID to every request |
| Logging | Logs request/response information |
| Tenant | Multi-tenant support |
Using Middleware¶
Enabling Middleware¶
Add middleware to your app configuration:
# settings.py
AKSARA = {
"MIDDLEWARE": [
"aksara.middleware.RequestIDMiddleware",
"aksara.middleware.LoggingMiddleware",
"aksara.middleware.TenantMiddleware",
]
}
Order matters! Middleware runs in the order listed for requests, and reverse order for responses.
Request ID Middleware¶
Adds a unique identifier to every request for tracking and debugging.
What it does:
- Generates a UUID for each request
- Adds it to request.state.request_id
- Includes it in response headers as X-Request-ID
Use it for: - Tracking requests across services - Finding related log entries - Debugging production issues
# Access in your code
@router.get("/test")
async def test(request: Request):
request_id = request.state.request_id
return {"request_id": request_id}
👉 Full Request ID Documentation
Logging Middleware¶
Automatically logs information about every request and response.
What it logs: - Request method and path - Response status code - Request duration - Request ID (if enabled)
Example log output:
Tenant Middleware¶
Enables multi-tenant applications where one codebase serves multiple customers.
What it does:
- Identifies the tenant from the request (subdomain, header, or path)
- Sets request.state.tenant
- Scopes database queries to that tenant
Tenant identification methods:
- Subdomain: acme.yourapp.com → tenant is "acme"
- Header: X-Tenant-ID: acme
- Path: /acme/api/users → tenant is "acme"
Writing Custom Middleware¶
Create your own middleware for custom logic:
from aksara.middleware import BaseMiddleware
from starlette.requests import Request
from starlette.responses import Response
class MyMiddleware(BaseMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
# Code that runs BEFORE your view
print(f"Request starting: {request.url}")
# Call the next middleware or your view
response = await call_next(request)
# Code that runs AFTER your view
print(f"Request finished: {response.status_code}")
return response
Middleware Structure¶
class MyMiddleware(BaseMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
# 1. Pre-processing (runs before your view)
# - Validate request
# - Add data to request.state
# - Reject request early if needed
# 2. Call next middleware or view
response = await call_next(request)
# 3. Post-processing (runs after your view)
# - Modify response
# - Add headers
# - Log information
return response
Example: API Key Authentication¶
from aksara.middleware import BaseMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
class APIKeyMiddleware(BaseMiddleware):
async def dispatch(self, request: Request, call_next):
# Skip authentication for certain paths
if request.url.path in ["/health", "/docs"]:
return await call_next(request)
# Check for API key
api_key = request.headers.get("X-API-Key")
if not api_key:
return JSONResponse(
{"error": "API key required"},
status_code=401
)
# Validate API key (you'd check against database)
if not await self.validate_api_key(api_key):
return JSONResponse(
{"error": "Invalid API key"},
status_code=403
)
# Store user info on request for later use
request.state.api_key = api_key
return await call_next(request)
async def validate_api_key(self, key: str) -> bool:
# Check if key exists in database
from myapp.models import APIKey
return await APIKey.objects.filter(key=key, is_active=True).exists()
Example: Response Time Header¶
import time
from aksara.middleware import BaseMiddleware
class ResponseTimeMiddleware(BaseMiddleware):
async def dispatch(self, request, call_next):
start_time = time.time()
response = await call_next(request)
# Calculate request duration
duration = time.time() - start_time
response.headers["X-Response-Time"] = f"{duration:.3f}s"
return response
Middleware Order¶
Middleware runs in order for requests and reverse order for responses:
MIDDLEWARE = [
"RequestIDMiddleware", # 1st for request, last for response
"LoggingMiddleware", # 2nd for request, 2nd-to-last for response
"AuthMiddleware", # 3rd for request, 3rd-to-last for response
]
Typical ordering:
MIDDLEWARE = [
# First: Request tracking
"aksara.middleware.RequestIDMiddleware",
# Second: Logging (to capture everything)
"aksara.middleware.LoggingMiddleware",
# Third: Authentication
"myapp.middleware.AuthMiddleware",
# Last: Business logic middleware
"myapp.middleware.TenantMiddleware",
]
Accessing Middleware Data¶
Data added by middleware is available on request.state:
# In your view
@router.get("/dashboard")
async def dashboard(request: Request):
# From RequestIDMiddleware
request_id = request.state.request_id
# From custom AuthMiddleware
user = request.state.user
# From TenantMiddleware
tenant = request.state.tenant
return {"user": user.id, "tenant": tenant.name}
Middleware vs Dependencies¶
| Middleware | Dependencies |
|---|---|
| Runs on every request | Runs only when declared |
| Global scope | Route-specific scope |
| Good for cross-cutting concerns | Good for route-specific logic |
| Cannot access route parameters | Can access route parameters |
Use middleware for: - Request ID, logging, authentication - Anything that should run on every request
Use dependencies for: - Getting current user for a specific route - Validating permissions on specific endpoints - Parsing route-specific data
Configuration Options¶
Some middleware accepts configuration:
# settings.py
AKSARA = {
"MIDDLEWARE": [
"aksara.middleware.RequestIDMiddleware",
],
# Middleware-specific settings
"REQUEST_ID_HEADER": "X-Request-ID", # Custom header name
"REQUEST_ID_GENERATOR": "uuid4", # ID generation method
"LOGGING_LEVEL": "INFO", # Logging verbosity
"LOGGING_INCLUDE_BODY": False, # Log request bodies?
"TENANT_HEADER": "X-Tenant-ID", # Tenant identification
"TENANT_STRATEGY": "header", # header, subdomain, or path
}
Middleware Documentation¶
| Guide | What You'll Learn |
|---|---|
| Request ID | Request tracking and debugging |
| Logging | Request/response logging |
| Tenant | Multi-tenant applications |
Reference¶
For complete API documentation: