Tier9AI logoTier9AI

Chapter 01

From Python Prototype to Reliable AI API

A practical guide to FastAPI, asynchronous work, stable errors, webhooks, API styles, and third-party SDK boundaries for production AI systems.

Peter Olson

8 min read

A Python prototype proves that an idea can work. A production API must prove something harder: that the idea behaves predictably when clients retry, providers slow down, inputs are malformed, and several requests arrive at once.

FastAPI is a strong starting point because it combines Python type hints, request validation, generated OpenAPI documentation, and support for asynchronous handlers. None of those features makes an API reliable by itself. Reliability comes from explicit contracts and clear boundaries around every operation that can fail.

Start with the contract, not the framework

Define the API from the caller's point of view before connecting a model. For every operation, write down:

  • the input schema and size limits;
  • the authentication and tenant context;
  • whether the request is synchronous or creates a job;
  • the success response and stable error codes;
  • the timeout and retry behavior; and
  • whether repeating the request is safe.

REST is usually the simplest fit for commands such as creating an ingestion job, starting an evaluation, or retrieving a known result. GraphQL can help when several clients need different views over a connected domain, but its flexibility also requires query-cost controls, authorization at resolver boundaries, and protection from expensive nested requests. Choose based on the client contract, not fashion. Many production systems use REST for commands and webhooks, while using GraphQL selectively for read-heavy product surfaces.

Pydantic models can validate shape and basic constraints at the FastAPI boundary. Domain rules still belong in a service layer that can be tested without HTTP. That separation also keeps route handlers small: authenticate, validate, call the application service, and translate its result into the public response.

Use async where waiting is the work

An async route helps when it awaits network or storage I/O and the libraries below it are genuinely asynchronous. It does not make CPU-heavy parsing, local model inference, or a synchronous SDK non-blocking. Running those tasks directly in the event loop can stall unrelated requests.

Keep short I/O-bound work in the request path. Put long, CPU-heavy, or failure-prone work behind a durable queue. A job-oriented endpoint can return 202 Accepted with a job identifier, while a worker performs ingestion, inference, or export. The client can poll a status endpoint or receive a webhook when the state changes.

Set explicit connection and read timeouts on every outbound call. A request without a timeout can consume capacity indefinitely. Apply bounded retries only to transient failures and only when the operation is idempotent. Backoff and jitter reduce synchronized retry storms.

Make errors useful and safe

Return a consistent error envelope with a machine-readable code, a safe message, a request identifier, and field-level validation details when appropriate. Do not expose stack traces, provider credentials, prompts containing customer data, or raw third-party responses.

Separate failures into categories that drive behavior:

  • 4xx for a caller action such as invalid input, missing permission, or a conflicting state;
  • 429 for an enforced limit, ideally with retry guidance;
  • 5xx for a server failure the caller did not cause; and
  • an accepted job state for work that is still processing, rather than holding a connection open.

Log the internal cause against the same request identifier returned to the client. This gives support and engineering a shared reference without leaking internals.

Treat webhooks as an untrusted delivery channel

Inbound webhooks should be authenticated using the provider's documented signature scheme over the raw request body. Validate the timestamp when the scheme includes one, reject stale replays, and compare signatures safely. A secret URL is not a substitute for signature verification.

After verification, store the provider's event identifier behind a unique constraint, acknowledge quickly, and process asynchronously. Providers can deliver events more than once or out of order. Your handler therefore needs idempotent state transitions, not an assumption of exactly-once delivery.

For outbound webhooks, sign messages, document the retry schedule, provide an event ID, and expose delivery status. A dead-letter queue gives operators somewhere to inspect events that exhausted their retry budget.

Put third-party SDKs behind adapters

Vendor SDKs save time, but letting their types and exceptions spread across the codebase makes later upgrades painful. Wrap each SDK in a narrow adapter owned by your application. Translate vendor responses into domain types, normalize errors, enforce timeouts, and add metrics at this boundary.

Pin versions and review changelogs before upgrades. Test the adapter against recorded fixtures or the vendor's sandbox, while keeping a small number of live integration checks for behavior a mock cannot prove.

Those adapters become the controlled seams for production customer integrations.

Common failure modes

  • Marking a route async while calling blocking libraries inside it.
  • Retrying every failure, including validation errors and non-idempotent writes.
  • Returning provider error text or sensitive request data to clients.
  • Processing a webhook before verifying its signature and recording its ID.
  • Coupling core business logic directly to a vendor SDK.
  • Offering GraphQL without depth, complexity, pagination, and resolver-level authorization controls.

Implementation checklist

  • Define versioned request, response, and error schemas.
  • Propagate request, tenant, job, and event identifiers through logs and traces.
  • Add timeouts, retry budgets, and circuit-breaking behavior around dependencies.
  • Move long-running work to a durable queue with observable job states.
  • Verify webhook signatures and make consumers idempotent.
  • Wrap each external SDK behind a tested application interface.
  • Publish an OpenAPI description and examples that match deployed behavior.
  • Load-test both the ordinary path and a degraded-provider path.

Measurable signals

Track p50, p95, and p99 latency by operation; error rate by stable error code; dependency timeout rate; queue age; job completion time; webhook duplicate rate; retry attempts; and the percentage of requests that can be correlated end to end. A useful launch target is not “zero errors.” It is known behavior under failure, with a measurable recovery path.

Further reading