Skip to content

Your first decorator

This tutorial shows you how to put a single route into maintenance mode and verify the behaviour. The examples use FastAPI — the currently supported ASGI adapter.

1. Create a simple app

app.py
from fastapi import FastAPI
from shield.core.engine import ShieldEngine
from shield.core.backends.memory import MemoryBackend
from shield.fastapi.middleware import ShieldMiddleware
from shield.fastapi.router import ShieldRouter
from shield.fastapi.decorators import maintenance, force_active

engine = ShieldEngine(backend=MemoryBackend())

app = FastAPI()
app.add_middleware(ShieldMiddleware, engine=engine)

router = ShieldRouter(engine=engine)

@router.get("/payments")
@maintenance(reason="Database migration — back at 04:00 UTC")
async def get_payments():
    return {"payments": []}

@router.get("/health")
@force_active
async def health():
    return {"status": "ok"}

app.include_router(router)

2. Run it

uv run uvicorn app:app --reload

3. Verify the behaviour

# /payments → 503 Maintenance
curl -s http://localhost:8000/payments | python -m json.tool
{
  "error": {
    "code": "MAINTENANCE_MODE",
    "message": "This endpoint is temporarily unavailable",
    "reason": "Database migration — back at 04:00 UTC",
    "path": "GET:/payments",
    "retry_after": null
  }
}
# /health → 200 (force_active bypasses all shield checks)
curl -s http://localhost:8000/health
{"status": "ok"}

How it works

@router.get("/payments")
@maintenance(reason="Database migration")
async def get_payments():
    ...
  1. @maintenance(...) stamps __shield_meta__ = {"status": "maintenance", "reason": "..."} on the function. The function itself is not modified; it still runs normally if called directly.

  2. When app.include_router(router) is called, ShieldRouter scans all routes for __shield_meta__ and calls engine.register() for each one.

  3. On every HTTP request, ShieldMiddleware calls engine.check(path). If the route is in maintenance, the engine raises MaintenanceException and the middleware returns a 503 response. The route handler never executes.


Available decorators

Decorator Behaviour
@maintenance(reason, start, end) 503, temporarily unavailable
@disabled(reason) 503, permanently off
@env_only("dev", "staging") 404 in other environments
@deprecated(sunset, use_instead) 200 + deprecation headers
@force_active Always 200, bypasses all checks
@rate_limit("100/minute") 429 when the limit is exceeded; requires api-shield[rate-limit]

Runtime changes without restart

Once the middleware is in place, you can change route state at runtime with no code changes and no restart:

# Enable the route programmatically
await engine.enable("GET:/payments")

# Put it back in maintenance
await engine.set_maintenance("GET:/payments", reason="Second migration wave")

Or via the CLI (requires ShieldAdmin mounted; see Admin Dashboard):

shield enable GET:/payments
shield maintenance GET:/payments --reason "Second migration wave"

Next step

Tutorial: Adding middleware →