Integrate image generation in FastAPI as a backend-only client with idempotency, explicit timeouts, and typed error handling.
“A dreamlike floating library arranged around a circular portal, lilac and gold shelves, soft cloud-blue atmosphere, conceptual editorial art”
FLUX Schnell · fast · 960 × 1472Keep the API route thin and the generation service testable
The HTTP route should parse the Pydantic model, resolve the API principal, read the idempotency and country headers, enforce a scope, and call the generation service. It should not know Runware task fields, Leonardo polling shapes, or Together response formats.
Provider adapters implement one interface. The service owns dimension normalization, safety, rate limits, candidate order, retry classification, circuit breakers, output moderation, cost estimates, idempotency, and usage logging. This division makes provider behavior unit-testable with mocked HTTP responses.
@router.post("/images/generations", response_model=GenerationResponse)
async def generate(
payload: GenerationRequest,
request: Request,
principal: Principal = Depends(get_principal),
idempotency_key: str | None = Header(None, alias="Idempotency-Key"),
):
require_scope(principal, "images:generate")
return await request.app.state.generation_service.generate(
payload, principal=principal, idempotency_key=idempotency_key,
country_code=request.headers.get("CF-IPCountry"),
)Use async I/O for provider calls, but bound concurrency explicitly
Async HTTP prevents a worker from blocking while an inference provider queues or generates. It does not create free capacity. A semaphore protects the process from accepting more simultaneous generations than memory, file descriptors, and provider limits can sustain.
Set provider timeouts and retry counts through server-side deployment configuration. Do not wrap an unbounded retry loop in an async endpoint. For long-running premium work, add a database-backed job queue and return 202 rather than keeping connections open across every deployment layer.
- One bounded timeout per provider attempt
- Small retry count with exponential backoff
- Semaphore or distributed concurrency control
- Circuit breaker for repeated transient failures
- Job mode for work beyond the synchronous budget
Return a stable error envelope independent of the upstream vendor
Clients need to distinguish invalid_api_key, invalid_dimensions, safety_rejection, rate_limit_exceeded, providers_exhausted, and safety_service_unavailable. Raw provider payloads are inconsistent and may leak account details.
The app converts internal exceptions into one JSON envelope with code, message, request ID, parameter, and sanitized attempt markers. Non-API 404s still render a helpful HTML page, while /v1 errors remain JSON.
| Layer | Raises | Public result |
|---|---|---|
| Pydantic | Validation error | 422 invalid_request |
| Safety | Policy or service error | 400 or 503 |
| Provider adapter | Classified provider error | Fallback or sanitized API error |
| Router | Exhausted candidates | 502 providers_exhausted |
Serve the marketing site with HTML already in the response
The same FastAPI process can serve Jinja2 pages whose primary content, title, canonical, breadcrumbs, FAQ, and JSON-LD are present without client-side rendering. This keeps the SEO layer simple and lets the API and marketing site share model and benchmark data.
Keep interactive tools progressively enhanced. The cost calculator and playground should be useful after JavaScript loads, but their explanatory content and internal links must already exist in server-rendered HTML.
Questions about FastAPI image generation API
Why not put the frontend in a separate SPA?
You can, but an SSR Jinja layer is smaller and easier to crawl for this API-first marketing site. A separate app adds deployment and SEO complexity without clear launch value.
Does async FastAPI make generation faster?
It improves server concurrency while waiting on providers. Model inference speed still depends on the provider and request.
Where are provider keys loaded?
From server-side environment variables or a secret manager. Provider keys are never sent to the browser.
Can I use multiple Uvicorn workers?
Yes after replacing the in-process rate limiter and semaphore with distributed controls. Database idempotency already supports shared persistence.