FastAPI Integration
The toggly-fastapi package provides native integration with FastAPI including middleware, dependency injection, route decorators, and full async support.
Installation
pip install toggly-fastapi
Requires Python 3.9+ (toggly-fastapi declares requires-python = ">=3.9").
The core toggly package still supports 3.8+.
Quick Start
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, Request
from toggly_fastapi import configure_toggly, TogglyMiddleware, get_toggly
@asynccontextmanager
async def lifespan(app: FastAPI):
client = configure_toggly(
app_key=os.environ.get("TOGGLY_APP_KEY"),
environment="Production",
feature_defaults={"new-homepage": False},
)
try:
yield
finally:
client.close()
app = FastAPI(lifespan=lifespan)
# Add middleware for request-scoped feature flags
app.add_middleware(TogglyMiddleware)
@app.get("/")
async def root(toggly = Depends(get_toggly)):
if toggly.is_enabled("new-homepage"):
return {"message": "Welcome to the new homepage!"}
return {"message": "Welcome!"}
Save the example as app.py, install an ASGI server such as uvicorn, and run uvicorn app:app. Set TOGGLY_APP_KEY in the process environment. Without a key, local defaults let the server start without fetching definitions. Create new-homepage in the configured application/environment, enable it, and revisit / after the next refresh (180 seconds by default).
The following route examples extend that app. Keep authentication and permission checks alongside feature gates.
Configuration
Pass these options to configure_toggly in the lifespan startup above:
client = configure_toggly(
app_key=os.environ.get("TOGGLY_APP_KEY"),
environment="Production",
refresh_interval=180.0,
connect_timeout=10.0,
request_timeout=30.0,
enable_usage_tracking=True,
disable_background_refresh=False,
)
With Pre-configured Client
from toggly import TogglyClient, TogglyConfig
from toggly_fastapi import configure_toggly
config = TogglyConfig(app_key="your-app-key")
client = TogglyClient(config)
client.init()
configure_toggly(client=client)
Run this initialization once in the lifespan startup and close the supplied client at shutdown. Use TogglyClient with native middleware and dependencies, whose evaluation methods are synchronous.
Middleware
TogglyMiddleware
The recommended middleware attaches a TogglyRequestHelper to each request:
from toggly_fastapi import TogglyMiddleware
app.add_middleware(TogglyMiddleware)
@app.get("/")
async def root(request: Request):
# Access via request.state
if request.state.toggly.is_enabled("my-feature"):
return {"enabled": True}
return {"enabled": False}
TogglyASGIMiddleware
A pure ASGI middleware alternative:
from toggly_fastapi import TogglyASGIMiddleware
app.add_middleware(TogglyASGIMiddleware)
Dependency Injection
get_toggly
The primary way to access feature flags in route handlers:
from fastapi import Depends
from toggly_fastapi import get_toggly, TogglyRequestHelper
@app.get("/dashboard")
async def dashboard(toggly: TogglyRequestHelper = Depends(get_toggly)):
if toggly.is_enabled("new-dashboard"):
return {"version": "v2"}
return {"version": "v1"}
Type Alias
Use the TogglyDep type alias for cleaner code:
from toggly_fastapi import TogglyDep
@app.get("/dashboard")
async def dashboard(toggly: TogglyDep):
if toggly.is_enabled("new-dashboard"):
return {"version": "v2"}
return {"version": "v1"}
require_feature
Require a feature to be enabled for a route:
from fastapi import Depends
from toggly_fastapi import require_feature
@app.get("/beta", dependencies=[Depends(require_feature("beta-access"))])
async def beta_view():
return {"message": "Beta content!"}
# With custom error
@app.get(
"/new-feature",
dependencies=[Depends(require_feature(
"new-feature",
status_code=503,
detail="This feature is coming soon!"
))]
)
async def new_feature():
return {"message": "New feature!"}
require_features
Require multiple features:
from fastapi import Depends
from toggly import FeatureRequirement
from toggly_fastapi import require_features
# All features required
@app.get(
"/admin",
dependencies=[Depends(require_features(["admin", "dashboard"]))]
)
async def admin_dashboard():
return {"role": "admin"}
# Any feature required
@app.get(
"/premium",
dependencies=[Depends(require_features(
["premium", "vip"],
requirement=FeatureRequirement.ANY
))]
)
async def premium_content():
return {"access": "premium"}
feature_enabled
Get the feature state as a boolean for conditional logic:
from fastapi import Depends
from toggly_fastapi import feature_enabled
@app.get("/checkout")
async def checkout(
new_checkout: bool = Depends(feature_enabled("new-checkout"))
):
if new_checkout:
return {"version": "v2", "message": "New checkout!"}
return {"version": "v1", "message": "Original checkout"}
get_evaluation_context
Access the evaluation context directly:
from fastapi import Depends
from toggly import EvaluationContext
from toggly_fastapi import get_evaluation_context
@app.get("/debug")
async def debug(context: EvaluationContext = Depends(get_evaluation_context)):
return {
"identity": context.identity,
"groups": context.groups,
"traits": context.traits,
}
FeatureGateDependency
A class-based dependency for complex gate logic:
from fastapi import Depends
from toggly_fastapi import FeatureGateDependency
feature_gate = FeatureGateDependency(
required=["base-feature"],
optional=["enhanced-mode", "experimental"]
)
@app.get("/api")
async def api_endpoint(gate: dict = Depends(feature_gate)):
response = {"mode": "basic"}
if gate.get("enhanced-mode"):
response["mode"] = "enhanced"
if gate.get("experimental"):
response["experimental"] = True
return response
Route Decorators
@feature_flag_required
from toggly_fastapi import feature_flag_required
@app.get("/new-dashboard")
@feature_flag_required("new-dashboard")
async def new_dashboard(request: Request):
return {"message": "New dashboard!"}
# With custom error
@app.get("/beta")
@feature_flag_required(
"beta",
status_code=404,
detail="Coming soon!"
)
async def beta_view(request: Request):
return {"message": "Beta content"}
# With fallback
async def old_checkout(request: Request):
return {"version": "v1"}
@app.get("/checkout")
@feature_flag_required("new-checkout", fallback=old_checkout)
async def new_checkout(request: Request):
return {"version": "v2"}
@feature_gate_required
from toggly import FeatureRequirement
from toggly_fastapi import feature_gate_required
@app.get("/admin")
@feature_gate_required(["admin", "dashboard"])
async def admin_dashboard(request: Request):
return {"message": "Admin dashboard"}
@app.get("/premium")
@feature_gate_required(
["premium", "vip"],
requirement=FeatureRequirement.ANY
)
async def premium_content(request: Request):
return {"message": "Premium content"}
feature_switch
Switch between two handlers based on feature state. Register a typed route wrapper so FastAPI recognizes the injected Request, then delegate to the native switch:
from fastapi import Request
from toggly_fastapi import feature_switch
async def new_checkout(request: Request):
return {"version": "v2"}
async def old_checkout(request: Request):
return {"version": "v1"}
switch_checkout = feature_switch("new-checkout", new_checkout, old_checkout)
@app.get("/checkout")
async def checkout(request: Request):
# The SDK still selects the handler using this request's feature context.
return await switch_checkout(request=request)
Use this example as an alternative to the other /checkout examples above. The switch callable accepts *args, **kwargs; registering it directly as a FastAPI route would expose those as required query parameters. The wrapper keeps them out of the route's OpenAPI parameters.
FeatureFlagRouter
Apply a feature flag to all routes in a router:
from fastapi import APIRouter
from toggly_fastapi import FeatureFlagRouter
router = APIRouter()
feature_router = FeatureFlagRouter(router, "beta-api")
@feature_router.get("/users")
async def list_users(request: Request):
return {"users": []}
@feature_router.post("/users")
async def create_user(request: Request):
return {"status": "created"}
# All routes in feature_router require "beta-api" to be enabled
app.include_router(router, prefix="/api/v2")
Entity context
Set request.state.toggly_entity before the helper's first evaluation. If a dependency or decorator gates on an entity, populate it before that gate runs, for example in application middleware. This example uses two demonstration Orders; map your authorized domain object's values in an application.
from fastapi import Request
from toggly import TogglyEntityContext
from toggly_fastapi import get_toggly_client
@app.get("/order-checkout")
async def order_checkout(request: Request):
request.state.toggly_entity = TogglyEntityContext(
kind="Order", key="order-123", attributes={"Vip": True, "Total": 120.0}
)
helper = request.state.toggly
enabled = helper.is_enabled("ExpressCheckout")
other = TogglyEntityContext(
kind="Order", key="order-456", attributes={"Vip": False, "Total": 20.0}
)
client = get_toggly_client()
other_enabled = client.is_enabled(
"ExpressCheckout", context=helper.context.with_entity(other)
) if client is not None else False
return {"express": enabled, "other_express": other_enabled}
Configure ExpressCheckout with an Order Context Property condition Vip == true to see opposite results. The same user identity and groups are preserved. The request helper caches its context; use a per-call with_entity copy for another Order instead of changing the request entity after a check. See Python entity mapping for domain mappers and optional startup schema registration.
User Context
The middleware extracts user context from an object in request.state.user. Your authentication middleware must set it before any helper evaluation or gate dependency. get_current_user below represents your application's authentication function:
# If your auth middleware sets request.state.user:
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
user = await get_current_user(request)
request.state.user = user
return await call_next(request)
# Automatic context extraction:
# - identity: user.id or user.sub (JWT)
# - groups: user.roles or user.groups or user.scopes
# - traits: user.email, path, method, client_host, user_agent
Claims and request filters
Default extraction places request metadata (including user agent) in traits. Structured segment filters read context.request, and UserClaims reads context.claims; map these explicitly and call the core client with that context.
from fastapi import Request
from toggly import HttpRequestMapper
from toggly_fastapi import get_context_from_request, get_toggly_client
@app.get("/browser-offer")
async def browser_offer(request: Request):
context = HttpRequestMapper.merge_into(
request.headers, get_context_from_request(request)
)
client = get_toggly_client()
enabled = client.is_enabled("browser-offer", context=context) if client else False
return {"enabled": enabled}
Only trust country headers from your proxy. Add verified claims with context.with_claims(...). Creating this context does not change native gate dependencies elsewhere in the route: evaluate the enriched context explicitly for those filters. Keep user context per request and never mutate a shared client's identity. Remote variants use a separate client-wide startup context; configure_toggly accepts enable_variants, identity, variant_groups and variant_claims before initialization.
Testing
Use a real offline TogglyClient in a test lifespan, with no app key, feature_defaults, disable_background_refresh=True, enable_live_updates=False and enable_usage_tracking=False. Register it with configure_toggly(client=client), then close it in the lifespan's finally block. TestClient must be a context manager to run lifespan startup and shutdown:
from fastapi.testclient import TestClient
# The test app's lifespan provides {"beta-access": False} as a local default.
with TestClient(app) as http:
response = http.get("/beta")
assert response.status_code == 403
Use a separate fixture with the flag enabled to assert HTTP 200. For targeting, load actual definitions in a snapshot provider, alternate authenticated users and Orders, and exercise concurrent requests across an await boundary.
Async client
Async route handlers can call the synchronous native request helper: evaluation uses locally loaded definitions. Keep TogglyClient for native middleware, gate dependencies and decorators. For an independently managed AsyncTogglyClient, use its awaited core methods directly; see Async Client Usage. Supplying an async client to synchronous native helpers does not make those helpers await evaluation.
Learning path
Run the FastAPI sample for a complete application with native routes, request context and client lifecycle wiring.
Start with /, add the require_feature dependency, then try the two-Order example. Browse the Samples catalog for runnable projects and their setup instructions. Read the adapter's middleware, dependencies and decorators to follow startup, request context and gate evaluation.