Skip to main content

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.