Route(API) lifecycle management for ASGI Python web frameworks — maintenance mode, environment gating, deprecation, rate limiting, admin panels, and more. No restarts required.
Warning
Early Access — api-shield is fully functional and ready to use. We are actively building on top of a solid foundation and your real-world experience is invaluable at this stage. If you have feedback, feature ideas, or suggestions, please open an issue — every voice helps shape the roadmap.
These features are framework-agnostic and available to any adapter.
| Feature | Description |
|---|---|
| ⚡ Zero-restart control | State changes take effect immediately — no redeployment or server restart needed |
| 🔄 Sync & async | Full support for both async def and plain def route handlers — use await engine.* or engine.sync.* |
| 🛡️ Fail-open by default | If the backend is unreachable, requests pass through. Shield never takes down your API |
| 🔌 Pluggable backends | In-memory (default), file-based JSON, or Redis for multi-instance deployments |
| 🖥️ Admin dashboard | HTMX-powered UI with live SSE updates — no JS framework required |
| 🖱️ REST API + CLI | Full programmatic control from the terminal or CI pipelines — works over HTTPS remotely |
| 📋 Audit log | Every state change is recorded: who, when, what route, old status → new status |
| ⏰ Scheduled windows | asyncio-native scheduler — maintenance windows activate and deactivate automatically |
| 🔔 Webhooks | Fire HTTP POST on every state change — built-in Slack formatter and custom formatters supported |
| 🚦 Rate limiting | Per-IP, per-user, per-API-key, or global counters — tiered limits, burst allowance, runtime mutation |
| Feature | Description |
|---|---|
| 🎨 Decorator-first DX | @maintenance, @disabled, @env_only, @force_active, @deprecated, @rate_limit — state lives next to the route |
| 📄 OpenAPI integration | Disabled / env-gated routes hidden from /docs; deprecated routes flagged; live maintenance banners in the Swagger UI |
| 🧩 Dependency injection | All decorators work as Depends() — enforce shield state per-handler without middleware |
| 🎨 Custom responses | Return HTML, redirects, or any response shape for blocked routes — per-route or app-wide default on the middleware |
| 🔀 ShieldRouter | Drop-in APIRouter replacement that auto-registers route metadata with the engine at startup |
uv add "api-shield[all]"
# or: pip install "api-shield[all]"FastAPI is the currently supported adapter. Litestar, Starlette, Quart, and Django (ASGI) are on the roadmap.
from fastapi import FastAPI
from shield.core.config import make_engine
from shield.fastapi import (
ShieldMiddleware, ShieldAdmin, apply_shield_to_openapi,
maintenance, env_only, disabled, force_active, deprecated,
)
engine = make_engine()
app = FastAPI()
app.add_middleware(ShieldMiddleware, engine=engine)
@app.get("/payments")
@maintenance(reason="DB migration — back at 04:00 UTC")
async def get_payments():
return {"payments": []}
@app.get("/health")
@force_active
async def health():
return {"status": "ok"}
apply_shield_to_openapi(app, engine)
app.mount("/shield", ShieldAdmin(engine=engine, auth=("admin", "secret")))GET /payments → 503 {"error": {"code": "MAINTENANCE_MODE", ...}}
GET /health → 200 always
Manage routes from the CLI — no code changes, no restarts:
shield config set-url http://localhost:8000/shield
shield login admin
shield status
shield enable GET:/payments
shield global enable --reason "Deploying v2" --exempt /health| Decorator | Effect | Status |
|---|---|---|
@maintenance(reason, start, end) |
Temporarily unavailable | 503 |
@disabled(reason) |
Permanently off | 503 |
@env_only("dev", "staging") |
Restricted to named environments | 404 elsewhere |
@deprecated(sunset, use_instead) |
Still works, injects deprecation headers | 200 |
@force_active |
Bypasses all shield checks | Always 200 |
@rate_limit("100/minute") |
Cap requests per IP, user, API key, or globally | 429 |
By default, blocked routes return a structured JSON error body. You can replace it with anything — HTML, a redirect, plain text, or your own JSON — in two ways:
Per-route — pass response= directly on the decorator:
from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse
from shield.fastapi import maintenance, disabled
def maintenance_page(request: Request, exc: Exception) -> HTMLResponse:
return HTMLResponse(
f"<h1>Down for maintenance</h1><p>{exc.reason}</p>", status_code=503
)
@router.get("/payments")
@maintenance(reason="DB migration", response=maintenance_page)
async def payments():
return {"payments": []}
@router.get("/orders")
@maintenance(reason="Upgrade in progress", response=lambda *_: RedirectResponse("/status"))
async def orders():
return {"orders": []}Global default — set once on ShieldMiddleware, applies to every route without a per-route factory:
app.add_middleware(
ShieldMiddleware,
engine=engine,
responses={
"maintenance": maintenance_page, # all maintenance routes
"disabled": lambda req, exc: HTMLResponse(
f"<h1>Gone</h1><p>{exc.reason}</p>", status_code=503
),
},
)Resolution order: per-route response= → global responses[...] → built-in JSON. The factory can be sync or async and receives the live Request and the ShieldException that triggered the block.
from shield.fastapi.decorators import rate_limit
@router.get("/public/posts")
@rate_limit("10/minute") # 10 req/min per IP
async def list_posts():
return {"posts": [...]}
@router.get("/users/me")
@rate_limit("100/minute", key="user") # per authenticated user
async def get_current_user():
...
@router.get("/reports")
@rate_limit( # tiered limits
{"free": "10/minute", "pro": "100/minute", "enterprise": "unlimited"},
key="user",
)
async def get_reports():
...Policies can be mutated at runtime without redeploying (shield rl and shield rate-limits are aliases):
shield rl set GET:/public/posts 20/minute # raise the limit live
shield rl reset GET:/public/posts # clear counters
shield rl hits # blocked requests logRequires api-shield[rate-limit]. Powered by limits.
api-shield is built on the ASGI standard. The core (shield.core) is completely framework-agnostic and has zero framework imports. Any ASGI framework can be supported — either via a Starlette BaseHTTPMiddleware (for Starlette-based frameworks) or a raw ASGI callable for frameworks like Quart and Django that implement the ASGI spec independently.
| Framework | Status | Adapter |
|---|---|---|
| FastAPI | ✅ Supported | shield.fastapi |
| Litestar | 🔜 Planned | — |
| Starlette | 🔜 Planned | — |
| Quart | 🔜 Planned | — |
| Django (ASGI) | 🔜 Planned | — |
Want support for another ASGI framework? Open an issue.
Important
WSGI support is out of scope for this project.
api-shield is an ASGI-native library. Bolting WSGI support in through shims or patches would require a persistent background event loop, thread-bridging hacks, and a fundamentally different middleware model — complexity that would compromise the quality and reliability of both layers.
WSGI framework support (Flask, Django, Bottle, …) will be delivered as a separate, dedicated project designed from the ground up for the synchronous request model. This keeps both projects clean, well-tested, and maintainable without trade-offs.
Watch this repo or open an issue to be notified when the WSGI companion project launches.
| Backend | Persistence | Multi-instance |
|---|---|---|
MemoryBackend |
No | No |
FileBackend |
Yes | No |
RedisBackend |
Yes | Yes |
For rate limiting in multi-worker deployments, use RedisBackend — counters are atomic and shared across all processes.
Full documentation at attakay78.github.io/api-shield
| Tutorial | Get started in 5 minutes |
| Decorators reference | All decorator options |
| Rate limiting | Per-IP, per-user, tiered limits |
| ShieldEngine reference | Programmatic control |
| Backends | Memory, File, Redis, custom |
| Admin dashboard | Mounting ShieldAdmin |
| CLI reference | All CLI commands |
| Production guide | Monitoring & deployment automation |