AI generation API FastAPI: build a small asynchronous submission service
·
FastAPI can expose a narrow image submission endpoint to an internal application. Use an asynchronous HTTP client so a waiting network request does not occupy a synchronous worker thread. The application still receives an asynchronous generation job: choosing async def does not make the generated image appear in the submission response.
1. Install the service dependencies
Use a virtual environment and install FastAPI, Uvicorn, and HTTPX. Set GENACE_API_KEY through your runtime environment. The example is intended for a trusted internal network during development. Add your application’s authentication and per-user allowance before making the service public.
python -m pip install fastapi uvicorn httpx
Create main.py with a request model that deliberately exposes only a prompt and supported aspect ratios. This gives your service a smaller contract than forwarding any JSON supplied by the caller.
import os
from typing import Literal
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class ImageRequest(BaseModel):
prompt: str = Field(min_length=1, max_length=2000)
aspect_ratio: Literal["1:1", "16:9", "9:16", "4:3", "3:4"] = "1:1"
@app.post("/images", status_code=202)
async def create_image(body: ImageRequest):
prompt = body.prompt.strip()
if not prompt:
raise HTTPException(422, "Prompt cannot be whitespace")
key = os.environ.get("GENACE_API_KEY")
if not key:
raise HTTPException(503, "Generation service is not configured")
try:
async with httpx.AsyncClient(timeout=45) as client:
response = await client.post(
"https://genace.ai/api/v1/images/generations",
headers={"Authorization": f"Bearer {key}"},
json={"model": "nano-banana", "prompt": prompt,
"aspect_ratio": body.aspect_ratio, "n": 1},
)
except httpx.RequestError:
raise HTTPException(502, "Submission outcome requires investigation")
try:
data = response.json()
except ValueError:
raise HTTPException(502, "Unreadable response; acceptance is unknown") from None
if not isinstance(data, dict):
raise HTTPException(502, "Invalid response; acceptance is unknown")
if not response.is_success:
error = data.get("error")
if not isinstance(error, dict):
error = {}
outcomes = {"UPSTREAM_SUBMISSION_FAILED": "rejected",
"UPSTREAM_SUBMISSION_UNCERTAIN": "uncertain"}
code, job_id = error.get("code"), error.get("job_id")
if (response.status_code == 502 and isinstance(code, str)
and code in outcomes and isinstance(job_id, str) and job_id.strip()):
raise HTTPException(502, {
"code": code, "job_id": job_id,
"submission_outcome": outcomes[code],
})
raise HTTPException(response.status_code, "Generation request failed; investigate before retry")
job_id, state = data.get("job_id"), data.get("state")
if (response.status_code != 202 or not isinstance(job_id, str) or not job_id.strip()
or state not in ("queued", "running", "succeeded", "failed", "canceled")):
raise HTTPException(502, "Invalid acceptance response; outcome is unknown")
return {"job_id": job_id, "state": state}
FastAPI HTTPException
serializes its detail under a top-level detail field. For the two recognized
502 envelopes, this wrapper therefore returns detail.job_id, detail.code,
and the application-defined detail.submission_outcome; these are not new
Genace fields. Persist that ID before responding in a production implementation,
then observe GET for reconciliation. Rejected submission can still have pending
credit compensation; uncertain submission keeps its hold and must not trigger an
automatic retry, refund, or cancellation workaround.
The wrapper omits arbitrary upstream message text. Malformed JSON, missing IDs, and unexpected successful bodies produce a controlled unknown outcome instead of a success with an empty identifier. HTTPX request errors also remain uncertain. Its numeric timeout bounds individual network phases, not a guaranteed total request deadline. This example creates a client per call for clarity; review lifespan-managed pooling and response-size limits in a production service.
2. Run locally and add persistence
Run uvicorn main:app --host 127.0.0.1 --port 8000 and inspect the request schema at your local /docs page. The two thousand character maximum belongs to this example’s application policy. Genace’s current Nano Banana schema requires a nonempty string but does not declare that particular maximum.
For a multi-user service, save the remote job identifier against your local user before returning it. Add a status handler that checks this mapping and requests GET /api/v1/jobs/{id}?wait=0. The upstream key identifies your Genace account, so your service must enforce its own user boundaries.
3. Keep polling outside submission
Avoid placing a ten minute polling loop in the POST handler or assuming a FastAPI background task is a durable queue. A process restart can interrupt application work. Persist the identifier and let a scheduled worker or explicit status route resume observation.
If the client times out during submission, do not automatically repeat the POST. The remote operation may already exist. The example reports an uncertain outcome so an operator can investigate instead of hiding a possible duplicate. For GET failures, retrying observation of the same known job is a separate decision.
Common mistakes and verification
Whitespace-only prompts pass a simple string length check, which is why this route strips and checks again. Do not treat a successful status HTTP response as proof of generation success; inspect state, error, and nullable outputs separately.
Verify malformed input never reaches your HTTP client, missing configuration yields a controlled error, and transport exceptions do not trigger a second submission. Exercise succeeded, failed, canceled, and pending status fixtures when adding observation. Confirm your deployment does not expose an unauthenticated spending endpoint.
Continue with Python output collection and environment variable setup when turning this small service into an application.
Continue with code and integration
Models covered on this page
Where these facts come from
- Editorial keyword hypothesis; search volume not measured
- codebase: src/app/api/v1/images/generations/route.ts
- codebase: src/app/api/v1/jobs/[id]/route.ts
- codebase: src/ai/api/errors.ts
- https://fastapi.tiangolo.com/tutorial/handling-errors/
- https://www.python-httpx.org/exceptions/