Skip to main content

FastAPI Custom Responses

Provides normalized response objects and error handling for FastAPI applications.

Features

  • One error envelope for every failure: validation, HTTPException, ValueError, and unhandled exceptions.
  • Pydantic validation errors rewritten as human-readable messages instead of raw error arrays.
  • A stable code naming the condition, typed as an enum.
  • fastapi_responses to build FastAPI's responses mapping, documenting your codes in OpenAPI.
  • Generic Response[T], SuccessResponse, and PaginatedResponse[T] envelopes for success payloads.
  • ErrorResponseModel as both the error body the handlers emit and the schema documenting it.
  • ErrorResponse.from_status_code for an error carrying a status's standard HTTP phrase.

Quick Start

pip install fastapi-custom-responses
from http import HTTPStatus
from fastapi_custom_responses import (
    EXCEPTION_HANDLERS,
    DefaultErrorCode,
    ErrorResponse,
    Response,
    fastapi_responses,
)
from fastapi import APIRouter, FastAPI
from enum import StrEnum
from pydantic import BaseModel

router = APIRouter()

app = FastAPI(
    title="API",
    description="My API",
    version="1.0.0",
    exception_handlers=EXCEPTION_HANDLERS,
)

class Data(BaseModel):
    example: str

class OrderErrorCode(StrEnum):
    ORDER_LOCKED = "order_locked"
    PAYMENT_DECLINED = "payment_declined"

@router.get(
    "/",
    response_model=Response[Data],
    responses=fastapi_responses({
        HTTPStatus.FORBIDDEN: OrderErrorCode,
        HTTPStatus.INTERNAL_SERVER_ERROR: DefaultErrorCode,
    }),
)
async def index() -> Response[Data]:
    """Index route."""

    return Response(
        success=True,
        data=Data(example="hello"),
    )

@router.get(
    "/return-error",
    responses=fastapi_responses({HTTPStatus.FORBIDDEN: OrderErrorCode}),
)
async def error_route() -> Response[Data]:
    """Error route."""

    raise ErrorResponse(
        error="This order is locked.",
        status_code=HTTPStatus.FORBIDDEN,
        code=OrderErrorCode.ORDER_LOCKED,
    )

Response Envelopes

Envelope Body
Response[T] { "success": true, "data": { ... } }
SuccessResponse { "success": true }
PaginatedResponse[T] { "success": true, "data": [ ... ], "meta": { "offset": 0, "limit": 10, "total": 1 } }

When using OpenAPI generators, use SuccessResponse instead of Response if your endpoint has no data to return.

Build a paginated response from a page of items and the bounds it was read with:

return PaginatedResponse.build_page(items, offset=offset, limit=limit, total=total)

Error Normalization

Register the handlers when you create the app:

from fastapi import FastAPI
from fastapi_custom_responses import EXCEPTION_HANDLERS

app = FastAPI(exception_handlers=EXCEPTION_HANDLERS)

Every error then normalizes into one JSON shape:

{
  "success": false,
  "error": "Human-readable error message",
  "code": "stable_error_identifier"
}

Handled Exception Types

Exception Status Code Code Behavior
ErrorResponse Custom (default 400) Yours, if you pass one Uses the provided error message directly
RequestValidationError 400 validation_error Pydantic validation errors are converted to human-readable messages (see below)
HTTPException From exception None Uses the exception detail; also covers the 404 and 405 the router raises itself
ValueError 400 invalid_value Uses str(exc) as the error message
ValidationError (Pydantic) 500 internal_error A model failed to validate inside your app; logged and reported generically so its details stay out of the body
Exception (catch-all) 500 internal_error Reports the status phrase so the exception stays out of the body

code is present when a condition was named — by you, or by one of the library's own handlers. It is absent otherwise, rather than restating the status.

Raising Errors

Raise ErrorResponse with a message and status code:

from http import HTTPStatus
from fastapi_custom_responses import ErrorResponse

raise ErrorResponse(error="Resource not found", status_code=HTTPStatus.NOT_FOUND)

Or create one from the status alone, which carries that status's standard HTTP phrase:

raise ErrorResponse.from_status_code(HTTPStatus.FORBIDDEN)
# { "success": false, "error": "Forbidden" }

The library ships no wording of its own, so pass error whenever the phrase is too terse for the reader:

raise ErrorResponse(error="That name is already taken", status_code=HTTPStatus.CONFLICT)
# { "success": false, "error": "That name is already taken" }

Validation Error Normalization

When a request fails Pydantic validation, FastAPI normally returns a verbose JSON array of raw Pydantic errors. With EXCEPTION_HANDLERS, these are automatically converted into concise, human-readable messages.

Before (default FastAPI):

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "email"],
      "msg": "Field required",
      "input": {}
    }
  ]
}

After (with EXCEPTION_HANDLERS):

{
  "success": false,
  "error": "Field 'email' is required",
  "code": "validation_error"
}

When multiple fields fail validation, messages are joined with periods:

{
  "success": false,
  "error": "Field 'email' is required. Field 'age' must be a valid integer",
  "code": "validation_error"
}

Supported Pydantic error types and their human-readable formats:

Error Type Example Message
missing Field 'name' is required
string_type Field 'name' must be a string
int_type / int_parsing Field 'age' must be a valid integer
float_type / float_parsing Field 'price' must be a valid number
bool_type / bool_parsing Field 'active' must be a boolean
enum Field 'status' must be one of: 'active' or 'inactive'
uuid_type / uuid_parsing Field 'id' must be a valid UUID
string_too_short Field 'name' must be at least 3 characters
string_too_long Field 'name' must be at most 50 characters
too_short / too_long Field 'items' must have at least 1 item
greater_than / less_than Field 'age' must be greater than 0
greater_than_equal / less_than_equal Field 'age' must be at least 18
value_error Invalid email format (uses the validator message directly)
json_invalid Invalid JSON in request body

Any unrecognized error types fall back to the Pydantic error message prefixed with the field name.

Error Codes

error is human-readable and may be reworded or localized. code is the stable identifier clients branch on. Declare your codes as a StrEnum, so a module that never imports this package can own them:

from enum import StrEnum

class OrderErrorCode(StrEnum):
    ORDER_LOCKED = "order_locked"
    PAYMENT_DECLINED = "payment_declined"

The library's own handlers name their conditions too; import DefaultErrorCode to branch on validation_error, invalid_value, and internal_error.

Pass a member when raising; both ErrorResponse and from_status_code accept it:

raise ErrorResponse(
    error="This order is locked",
    status_code=HTTPStatus.FORBIDDEN,
    code=OrderErrorCode.ORDER_LOCKED,
)
# { "success": false, "error": "This order is locked", "code": "order_locked" }

raise ErrorResponse.from_status_code(HTTPStatus.FORBIDDEN, code=OrderErrorCode.ORDER_LOCKED)

Documenting Responses

fastapi_responses builds FastAPI's responses mapping. Give it an error code enum, a union of enums, None for the bare error envelope, or a success envelope:

from fastapi_custom_responses import DefaultErrorCode, Response, SuccessResponse, fastapi_responses

@router.post(
    "/reports",
    responses=fastapi_responses({
        HTTPStatus.CREATED: Response[Report],
        HTTPStatus.ACCEPTED: SuccessResponse,
        HTTPStatus.BAD_REQUEST: DefaultErrorCode,
        HTTPStatus.FORBIDDEN: OrderErrorCode,
        HTTPStatus.NOT_FOUND: None,
    }),
)

Each error code enum becomes its own named schema in the OpenAPI document, so generated clients get a real union type per domain rather than a bare string:

"OrderErrorCode": { "type": "string", "enum": ["order_locked", "payment_declined"], "title": "OrderErrorCode" }

400 and 500 are answered by the library's own handlers, so document DefaultErrorCode there. Where a status carries your codes as well as theirs, union the two — OrderErrorCode | DefaultErrorCode — so the schema lists every value that status can emit.

FastAPI describes each entry with its status phrase. Entries needing headers, custom media types, or links are written directly and merge with the result:

responses={**fastapi_responses({HTTPStatus.FORBIDDEN: OrderErrorCode}), HTTPStatus.NOT_MODIFIED: {"headers": {...}}}

Local Development

Install the project with its development dependencies:

poetry install -E dev

Run the test suite:

poetry run pytest tests/ -v

Format and lint:

poetry run black .
poetry run isort .
poetry run pylint fastapi_custom_responses/ .github/scripts/

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_custom_responses-0.2.0.tar.gz (7.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_custom_responses-0.2.0-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_custom_responses-0.2.0.tar.gz.

File metadata

  • Download URL: fastapi_custom_responses-0.2.0.tar.gz
  • Upload date:
  • Size: 7.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for fastapi_custom_responses-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2dc206fff3c2fb86bad78c4095ac7bdeecddadb6bca44fb8cfc40baf60a7c9d9
MD5 b322f79bc60ff2529ca2b1674991a95b
BLAKE2b-256 b62b9f3c64641176dfc331c5dc726af05f13010c12f73e5e4f630ae03e500339

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_custom_responses-0.2.0.tar.gz:

Publisher: publish-to-pypi.yml on julien777z/fastapi-custom-responses

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastapi_custom_responses-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_custom_responses-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 74cb7f1433ab57e2987f30a235c1debbdd795d37aeb8ac69f04ea12c09f3dd01
MD5 6705d1eeccab8fa8b0afeb9ef9a4ab2f
BLAKE2b-256 1f4fd5591c6d6790659a7bee4522260a3e23d40bbc4c5fedd59ae27c015b9fd0

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_custom_responses-0.2.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on julien777z/fastapi-custom-responses

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.15

2 files

0.0.14

2 files

0.0.13

2 files

0.0.12

2 files

0.0.11

2 files

0.0.10

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

0.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page