DocumentationMiddleware, throttling, and docs protection
API referenceGitHub

Middleware, throttling, and docs protection

Add security and observability middleware, rate limits, IP cooldowns, CSRF checks, and private Swagger docs.

Work through each step in order. The examples use the generated LenAPI project structure and are designed to be adapted to your app.

01

How middleware works

Middleware is optional ASGI code that wraps requests and responses around your routes. Add it with app.add_middleware; the first added middleware wraps the later ones. LenAPI's built-ins include CORS, gzip compression, request IDs, timing headers, and documentation authentication. Add only the middleware your application needs.

  • CORSMiddleware controls which browser origins, methods, and headers are allowed. Use explicit frontend origins in production; credentialed requests cannot use a wildcard origin.
  • GZipMiddleware compresses eligible response bodies when the client accepts gzip. minimum_size sets the size threshold; compresslevel ranges from 0 through 9.
  • RequestIDMiddleware accepts a safe request ID or creates one, then returns it in X-Request-ID for log correlation.
  • TimingMiddleware returns app processing time in X-Process-Time-Ms; it is not full network latency.
  • DocsAuthMiddleware protects configured Swagger, ReDoc, and OpenAPI paths using a callable authenticator.
Python
from lenapi.middleware import (
    CORSMiddleware, GZipMiddleware, RequestIDMiddleware, TimingMiddleware,
)

app.add_middleware(RequestIDMiddleware)
app.add_middleware(TimingMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://frontend.example"],
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["authorization", "content-type"],
)
02

Configure CORS for a browser client

CORS is a browser policy, not API authentication. List the frontend origin and allow the methods and headers it actually sends. Expose response headers if browser JavaScript needs to read them. If cookies are used, enable allow_credentials and list exact origins instead of using a wildcard.

Python
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://frontend.example"],
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["authorization", "content-type", "X-Flutterwave"],
    expose_headers=["X-Request-ID", "X-Process-Time-Ms"],
    allow_credentials=False,
)
TIP

If the frontend uses cookie authentication, set allow_credentials=True and keep allow_origins as an explicit list of origins. CORS does not replace authentication or CSRF protection.

03

Add compression and request observability

GZipMiddleware compresses eligible text, JSON, XML, JavaScript, WebAssembly, and SVG response types when Accept-Encoding permits it. It skips content that is already encoded or partial. Request ID and timing middleware add response headers that can be used by clients and logs.

Python
app.add_middleware(RequestIDMiddleware)
app.add_middleware(TimingMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500, compresslevel=6)

# Example response headers:
# X-Request-ID: 9af...
# X-Process-Time-Ms: 2.731
# Content-Encoding: gzip  (when response qualifies)
TIP

Compression uses Python's standard library. It does not add a Starlette dependency. Choose a sensible minimum_size to avoid spending CPU compressing tiny responses.

04

Choose database or Redis for shared security state

Set REDIS_URL to the Redis server connection string. CACHE_BACKEND controls app.cache only; SECURITY_STORAGE independently controls shared sessions, API throttling/IP cooldown, opaque tokens, and HMAC nonce replay checks. To put both cache and security state in Redis, set both backend settings. Redis must be installed and running separately from the Python client package.

Python
# settings.py
REDIS_URL = "redis://127.0.0.1:6379/0"
CACHE_BACKEND = "redis"
SECURITY_STORAGE = "redis"

# Install the Python Redis client in this project's environment:
python -m pip install 'lenapi[redis]'

# Confirm the Redis server responds:
redis-cli ping  # PONG
TIP

The URL format is redis://host:port/database-number; this uses localhost, port 6379, database 0. The quoted brackets are important in zsh. Installing lenapi[redis] adds the Python client, not the Redis server. If you want the cache in Redis but security state in SQL, leave SECURITY_STORAGE='database'.

05

Configure rate limits

Set app-level limits on LENAPI to provide defaults for routes. A route or router with its own throttle mapping replaces the inherited mapping; it does not merge with it. Repeat any global scopes you still want enforced on that route. ip counts by client IP, anonymous counts unauthenticated calls, and authenticated counts a verified identity from auth=.

Python
app = LENAPI(throttle={
    "ip": "300/minute",
    "anonymous": "30/minute",
    "authenticated": "600/minute",
})

# Uses the app-level defaults.
@app.get("/items")
def items():
    return {"items": []}

# Replaces the defaults for /login. Keep the scopes needed here.
@app.post("/login", throttle={
    "ip": "5/minute",
    "anonymous": "30/minute",
})
def login(data: LoginInput):
    return verify_credentials(data)
TIP

If login used only throttle={"ip": "5/minute"}, only that route-specific limit would apply; the global anonymous limit would not. An explicit throttle={} disables inherited limits. BasicAuth has a default 10-per-minute IP limit unless a pre-auth IP/anonymous limit is configured. Process-memory counters do not coordinate across workers.

06

Use a cooldown for sensitive endpoints

api_flag enables a per-IP cooldown. The configured security backend stores the flag and returns 429 plus Retry-After while the cooldown is active.

Python
@app.post("/webhooks/receive", api_flag=True, cool_time=300)
def receive(payload: EventInput):
    return {"received": True}
07

Protect cookie-authenticated writes against CSRF

CSRFProtect issues a signed token cookie and checks the matching X-CSRF-Token header on state-changing requests. SameSite cookies are useful defense in depth, not a replacement for the token.

Python
from lenapi.security import CSRFProtect

csrf = CSRFProtect(settings.SECRET_KEY, secure=True, samesite="lax")

@app.post("/account/email")
def change_email(user=Depends(app.authenticator), _csrf=Depends(csrf)):
    return {"updated": True}
08

Make Swagger documentation private

Set SAFE_GUARD_DOCS_URL to True to show an admin sign-in page at the base URL and protect Swagger/OpenAPI with database-backed admin sessions. Create an admin after the first migration.

Python
# settings.py
SAFE_GUARD_DOCS_URL = True

# First run
python lenora.py migration
python lenora.py admin
TIP

This protects documentation endpoints only. It does not automatically secure application API routes.