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
codenaming the condition, typed as an enum. fastapi_responsesto build FastAPI'sresponsesmapping, documenting your codes in OpenAPI.- Generic
Response[T],SuccessResponse, andPaginatedResponse[T]envelopes for success payloads. ErrorResponseModelas both the error body the handlers emit and the schema documenting it.ErrorResponse.from_status_codefor 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2dc206fff3c2fb86bad78c4095ac7bdeecddadb6bca44fb8cfc40baf60a7c9d9
|
|
| MD5 |
b322f79bc60ff2529ca2b1674991a95b
|
|
| BLAKE2b-256 |
b62b9f3c64641176dfc331c5dc726af05f13010c12f73e5e4f630ae03e500339
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_custom_responses-0.2.0.tar.gz -
Subject digest:
2dc206fff3c2fb86bad78c4095ac7bdeecddadb6bca44fb8cfc40baf60a7c9d9 - Sigstore transparency entry: 2512212926
- Sigstore integration time:
-
Permalink:
julien777z/fastapi-custom-responses@1280b502e45f4b5b4e24e1ac6ac74614154994e8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/julien777z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@1280b502e45f4b5b4e24e1ac6ac74614154994e8 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file fastapi_custom_responses-0.2.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_custom_responses-0.2.0-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74cb7f1433ab57e2987f30a235c1debbdd795d37aeb8ac69f04ea12c09f3dd01
|
|
| MD5 |
6705d1eeccab8fa8b0afeb9ef9a4ab2f
|
|
| BLAKE2b-256 |
1f4fd5591c6d6790659a7bee4522260a3e23d40bbc4c5fedd59ae27c015b9fd0
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_custom_responses-0.2.0-py3-none-any.whl -
Subject digest:
74cb7f1433ab57e2987f30a235c1debbdd795d37aeb8ac69f04ea12c09f3dd01 - Sigstore transparency entry: 2512213072
- Sigstore integration time:
-
Permalink:
julien777z/fastapi-custom-responses@1280b502e45f4b5b4e24e1ac6ac74614154994e8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/julien777z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@1280b502e45f4b5b4e24e1ac6ac74614154994e8 -
Trigger Event:
workflow_dispatch
-
Statement type: