Skip to content
AB

Designing a Stable API Envelope in FastAPI

Give mobile and web clients one predictable success, error, retry, and request-correlation contract.

Ashish BhakhandPublished

Clients become complicated when every endpoint expresses errors differently. One returns detail, another returns a validation array, and an unexpected exception returns HTML.

I use one envelope for non-streaming responses:

Code
{
  "ok": true,
  "data": { "id": "session-1" },
  "request_id": "01J..."
}
Code
{
  "ok": false,
  "error": {
    "code": "validation_error",
    "message": "Request validation failed.",
    "retryable": false,
    "details": []
  },
  "request_id": "01J..."
}

code is the stable machine contract. message can improve without requiring a client release. retryable lets the client decide whether retry is reasonable without interpreting status codes differently in every feature.

Apply success wrapping once#

A custom APIRoute can wrap successful JSON responses while leaving existing envelopes and non-JSON responses unchanged:

Code
class ApiEnvelopeRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original = super().get_route_handler()

        async def handler(request: Request) -> Response:
            response = await original(request)
            return maybe_wrap_success(request, response)

        return handler

Global handlers translate HTTPException, Pydantic validation errors, and unknown exceptions into the error envelope.

Do not leak internal failures#

An unknown exception returns a generic internal_error. Logs and error tracking retain the exception, stack, and request ID. Database messages and provider responses should not become public text.

Treat streaming as a separate protocol#

SSE responses cannot be wrapped after the stream begins. Define typed events such as ack, text, done, and error, and carry the request ID in headers and event payloads.

Test the contract centrally#

Use a minimal application to verify dictionaries, lists, Pydantic models, validation failures, handled errors, unexpected errors, headers, and streaming bypass. Feature tests can then focus on their own behavior.

A consistent envelope is valuable only when it is automatic. If every developer must remember to construct it manually, the contract will eventually drift.

← Back to all writing