Production API contracts and idempotency
Define validation, durable retry semantics and failure tests for a production API.
Published by DME with AI assistance. Examples are educational and identify their assumptions and limitations. Editorial approach and corrections.
A production API needs a predictable contract for success, validation errors and retries. A server can commit a write and lose the response before the caller sees it. If the caller retries a payment, order or job creation blindly, a duplicate side effect can follow. This guide uses a fictional job-creation endpoint to explain request validation and durable idempotency.
Define the request before implementation
Specify required fields, length limits, permitted values and whether unknown fields are rejected. Separate the public response model from the database model so internal fields do not leak through serialization. Document which errors are safe to retry. A timeout does not prove that the server did nothing; it means the caller does not know the result.
For our example, a client submits a job title and an idempotency key. The server scopes the key to the authenticated account and operation. A caller must not be able to use another account's key to retrieve its response. Set limits on key and payload size, and define how long the server retains results.
A minimal FastAPI validation boundary
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
app = FastAPI()
class JobDraft(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(min_length=1, max_length=120)
class JobPreview(BaseModel):
title: str
status: str
@app.post("/job-previews", response_model=JobPreview)
def preview_job(draft: JobDraft):
return JobPreview(title=draft.title, status="preview")
Install FastAPI in an isolated environment, save this as main.py, and use the official development command for your installed version. This endpoint performs validation and returns a preview; it does not persist jobs or implement authentication. An empty title or unknown field should return a validation error. This deliberate boundary avoids disguising an in-memory dictionary as durable production storage.
Persist the key and result together
A durable implementation needs a unique constraint on (account, operation, key). Store a normalized request fingerprint, processing state and result. The normalization must be stable: raw JSON bytes can differ while representing the same request. Reusing a key with a different payload should produce a clear conflict rather than return an unrelated old result.
BEGIN TRANSACTION
reserve (account, operation, key) under a unique constraint
compare normalized request fingerprint on an existing record
create the job if this transaction owns the reservation
store the response status and public response payload
COMMIT
return the stored result on an identical retry
Concurrent requests must converge on one result. A check-then-insert sequence without a database constraint is vulnerable to races. If a duplicate arrives while processing is in progress, define whether it waits, receives a retry response or polls a status endpoint. Never imply completion before the operation has actually reached a durable state.
Handle external side effects explicitly
A database transaction cannot atomically include a remote email service or payment API. Use an outbox or another delivery design suited to the system, and pass a downstream idempotency key where supported. Track retries and reconcile uncertain outcomes. “Exactly once” is not a property gained by adding a header; it depends on the entire operation and its failure model.
Test failures, not just the happy path
- Send the same request twice and verify one durable job.
- Reuse a key with a different payload and expect a conflict.
- Send concurrent identical requests and inspect the database result.
- Interrupt the response after commit and retry.
- Fail a downstream call and check recovery behavior.
- Verify that another account cannot obtain the stored response.
Add request identifiers to logs without recording credentials. Measure endpoint latency, error categories and retry frequency. Bound slow external calls with timeouts, and avoid blocking network work inside an asynchronous handler. A preview endpoint is a starting point for contract testing, not a substitute for these operational checks.
Technical reference
FastAPI's official tutorial covers models, responses, errors and testing. See Cloud modernization for data migration boundaries and Bounded agents for tool authorization.