Consistent JSON response formatting and exception & error handling for FastAPI applications
Project description
Standardising FastAPI responses with clarity, consistency, and control.
APIException: Standardised Exception Handling for FastAPI
APIException is a robust, production-ready Python library for FastAPI that simplifies exception handling and ensures consistent, well-structured API responses. Designed for developers who want to eliminate boilerplate error handling and improve Swagger/OpenAPI documentation, APIException makes your FastAPI projects cleaner and easier to maintain.
- 🔒 Consistent JSON responses for both success and errors.
- 📚 Beautiful Swagger/OpenAPI documentation with clear error cases.
- ⚙️ Customizable error codes with
BaseExceptionCode. - 🔗 Global fallback for unhandled server-side errors.
- 🗂️ Use with multiple FastAPI apps.
- 📜 Automatic logging of every exception detail.
- ✔️ Production-ready with unit test examples.
Reading the full documentation is highly recommended — it’s clear, thorough, and helps you get started in minutes.
[!IMPORTANT] New in v0.2.1:
- ✨ Async support for
extra_log_fields→ you can now useawait request.body()directly.- 🧩 Python 3.9 compatibility restored with
typing_extensions.TypeGuard.- ⚡ Improved
response_utils.pytype-safety for all Python versions.- 📦 Updated dependencies and
pyproject.tomlfor wider environment support.Previously in v0.2.0:
- Advanced structured logging (
log_level,log_header_keys,extra_log_fields)- Response headers echo (
response_headers)- Type-safety improvements with
mypyAPIExceptionacceptsheadersparam- Cleaner import/export structure
- 📢 Featured in Python Weekly #710 🎉
- 👉 For full details and usage examples, see register_exception_handlers reference
📦 Installation via pip
pip install apiexception
⚡ Quickstart: How to Integrate APIException
1️⃣ Register the Handler
from api_exception import register_exception_handlers, logger
from fastapi import FastAPI
app = FastAPI()
register_exception_handlers(app) # uses ResponseModel by default
logger.setLevel("INFO") # Set logging level if needed
🔍 Example: Error Handling with Custom Codes
from typing import List, Optional, Any, Dict
from fastapi import FastAPI, Path, Request
from pydantic import BaseModel, Field
from api_exception import (
APIException,
BaseExceptionCode,
ResponseModel,
register_exception_handlers,
APIResponse,
logger,
ResponseFormat
)
logger.setLevel("DEBUG")
app = FastAPI()
def my_extra_fields(request: Request, exc: Optional[BaseException]) -> Dict[str, Any]:
user_id = request.headers.get("x-user-id", "anonymous")
return {
"masked_user_id": f"user-{user_id[-2:]}",
"service": "billing-service",
"has_exc": exc is not None,
"exc_type": type(exc).__name__ if exc else None,
}
register_exception_handlers(app,
response_format=ResponseFormat.RESPONSE_MODEL,
log_traceback=True,
log_traceback_unhandled_exception=False,
log_level=10,
log=True,
response_headers=("x-user-id",),
log_request_context=True,
log_header_keys=("x-user-id",),
extra_log_fields=my_extra_fields
)
# Define your custom exception codes extending BaseExceptionCode
class CustomExceptionCode(BaseExceptionCode):
USER_NOT_FOUND = ("USR-404", "User not found.", "The user ID does not exist.")
INVALID_API_KEY = ("API-401", "Invalid API key.", "Provide a valid API key.")
PERMISSION_DENIED = ("PERM-403", "Permission denied.", "Access to this resource is forbidden.")
# Let's assume you have a UserModel that represents the user data
class UserModel(BaseModel):
id: int = Field(...)
username: str = Field(...)
# Create the validation model for your response.
class UserResponse(BaseModel):
users: List[UserModel] = Field(..., description="List of user objects")
@app.get("/user/{user_id}",
response_model=ResponseModel[UserResponse],
responses=APIResponse.default()
)
async def user(user_id: int = Path()):
if user_id == 1:
raise APIException(
error_code=CustomExceptionCode.USER_NOT_FOUND,
http_status_code=401,
)
if user_id == 3:
a = 1
b = 0
c = a / b # This will raise ZeroDivisionError and be caught by the global exception handler
return c
users = [
UserModel(id=1, username="John Doe"),
UserModel(id=2, username="Jane Smith"),
UserModel(id=3, username="Alice Johnson")
]
data = UserResponse(users=users)
return ResponseModel[UserResponse](
data=data,
description="User found and returned."
)
The above code demonstrates how to handle exceptions in FastAPI using the APIException library.
When you run your FastAPI app and open Swagger UI (/docs),
your endpoints will display clean, predictable response schemas like this below:
- Successful API Response?
{
"data": {
"users": [
{
"id": 1,
"username": "John Doe"
},
{
"id": 2,
"username": "Jane Smith"
},
{
"id": 3,
"username": "Alice Johnson"
}
]
},
"status": "SUCCESS",
"message": "Operation completed successfully.",
"error_code": null,
"description": "User found."
}
- Error API Response?
{
"data": null,
"status": "FAIL",
"message": "User not found.",
"error_code": "USR-404",
"description": "The user ID does not exist."
}
In both error and the success cases, the response structure is consistent.
- In the example above, when the
user_idis1, it raises anAPIExceptionwith a customerror_code, the response is formatted according to theResponseModeland it's logged automatically as shown below:
- Uncaught Exception API Response?
What if you forget to handle an exception such as in the example above?
- When the
user_idis3, the program automatically catches theZeroDivisionErrorand returns a standard error response, logging it in a clean structure as shown below:
{
"data": null,
"status": "FAIL",
"message": "Something went wrong.",
"error_code": "ISE-500",
"description": "An unexpected error occurred. Please try again later."
}
2️⃣ Raise an Exception
from api_exception import APIException, ExceptionCode, register_exception_handlers
from fastapi import FastAPI
app = FastAPI()
register_exception_handlers(app)
@app.get("/login")
async def login(username: str, password: str):
if username != "admin" or password != "admin":
raise APIException(
error_code=ExceptionCode.AUTH_LOGIN_FAILED,
http_status_code=401
)
return {"message": "Login successful!"}
3️⃣ Use ResponseModel for Success Responses
from api_exception import ResponseModel, register_exception_handlers
from fastapi import FastAPI
app = FastAPI()
register_exception_handlers(app)
@app.get("/success")
async def success():
return ResponseModel(
data={"foo": "bar"},
message="Everything went fine!"
)
Response Model In Abstract:
🧩 Custom Error Codes
Always extend BaseExceptionCode — don’t subclass ExceptionCode directly!
from api_exception import BaseExceptionCode
class CustomExceptionCode(BaseExceptionCode):
USER_NOT_FOUND = ("-404", "User not found.", "User does not exist.")
INVALID_API_KEY = ("API-401", "Invalid API key.", "Key missing or invalid.")
And use it:
from api_exception import APIException
raise APIException(
error_code=CustomExceptionCode.USER_NOT_FOUND,
http_status_code=404
)
⚙️ Override Default HTTP Status Codes
from api_exception import set_default_http_codes
set_default_http_codes({
"FAIL": 422,
"WARNING": 202
})
🌐 Multiple Apps Support
from fastapi import FastAPI
from api_exception import register_exception_handlers
mobile_app = FastAPI()
admin_app = FastAPI()
merchant_app = FastAPI()
register_exception_handlers(mobile_app)
register_exception_handlers(admin_app)
register_exception_handlers(merchant_app)
📝 Automatic Logging
Every APIException automatically logs:
-
File name & line number
-
Error code, status, message, description
Or use the built-in logger:
from api_exception import logger
logger.info("Custom info log")
logger.error("Custom error log")
logger.setLevel("DEBUG") # Set logging level
Examples
All in One Example Application
Below is a comprehensive example application demonstrating the capabilities of api_exception.
This single file showcases how you can:
- Work with multiple FastAPI apps (API, Mobile, Admin) in the same project
- Set different log levels based on the environment (e.g., INFO in dev, ERROR in prod)
- Enable or disable tracebacks per application
- Fully control logging behavior when raising
APIException(log or skip logging) - Customize
DEFAULT_HTTP_CODESto match your own status code mappings - Create and use custom exception classes with clean and consistent logging across the project
- Use
APIResponse.custom()andAPIResponse.default()for flexible response structures - Demonstrate RFC 7807 problem details integration for standards-compliant error responses
This example serves as a one-stop reference to see how api_exception can be integrated into a real-world project while keeping exception handling consistent, configurable, and developer-friendly.
✅ Testing Example
import unittest
from api_exception import APIException, ExceptionCode, ResponseModel
class TestAPIException(unittest.TestCase):
def test_api_exception(self):
exc = APIException(error_code=ExceptionCode.AUTH_LOGIN_FAILED)
self.assertEqual(exc.status.value, "FAIL")
def test_response_model(self):
res = ResponseModel(data={"foo": "bar"})
self.assertEqual(res.status.value, "SUCCESS")
if __name__ == "__main__":
unittest.main()
Run the Tests
- To run the tests, you can use the following command in your terminal:
python -m unittest discover -s tests
🔗 Full Documentation
Find detailed guides and examples in the official docs.
📊 Benchmark
We benchmarked apiexception's APIException against FastAPI's built-in HTTPException using Locust with 200 concurrent users over 2 minutes.
This can be used as a foundation. Can be extended to include more detailed tests.
| Metric | HTTPException (Control App) | APIException (Test App) |
|---|---|---|
| Avg Latency | 2.00 ms | 2.72 ms |
| P95 Latency | 5 ms | 6 ms |
| P99 Latency | 9 ms | 19 ms |
| Max Latency | 44 ms | 96 ms |
| Requests per Second (RPS) | ~608.88 | ~608.69 |
Failure Rate (/error) |
100% (intentional) | 100% (intentional) |
Analysis
- Both implementations achieved almost identical throughput (~609 RPS).
- In this test, APIException’s average latency was only +0.72 ms higher than HTTPException (2.42 ms vs 2.00 ms).
- The P95 latencies were nearly identical at 5 ms and 6 ms, while the P99 and maximum latencies for APIException were slightly higher but still well within acceptable performance thresholds for APIs.
Important Notice:APIExceptionautomatically logs exceptions, while FastAPI’s built-inHTTPExceptiondoes not log them by default. Considering the extra logging feature, these performance results are very strong, showing that APIException delivers standardized error responses, cleaner exception handling, and logging capabilities without sacrificing scalability.
Benchmark scripts and raw Locust reports are available in the benchmark directory.
📜 Changelog
Currently, the most stable and suggested version is v0.2.1
License
This project is licensed under the MIT License. See the LICENSE file for more details. If you like this library and find it useful, don’t forget to give it a ⭐ on GitHub!
Contact
If you have any questions or suggestions, please feel free to reach out at ahmetkutayural.dev Don't forget to add your email to the contact form!
📖 Learn More
📚 Full APIException Documentation
https://akutayural.github.io/APIException/
🐍 PyPI
https://pypi.org/project/apiexception/
💻 Author Website
https://ahmetkutayural.dev
🌍 Community & Recognition
- 📢 Featured in Python Weekly #710 🎉
- 🔥 Ranked #3 globally in r/FastAPI under the pip package flair.
- ⭐ Gaining traction on GitHub with developers adopting it for real-world FastAPI projects.
- 💬 Actively discussed and shared across the Python community.
Project details
Release history Release notifications | RSS feed
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 apiexception-0.2.2.tar.gz.
File metadata
- Download URL: apiexception-0.2.2.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88d99486d4ea9277bc294958b3bcca004e0490700d9974a36173f84aaa136cba
|
|
| MD5 |
4a8933cbf400a331f5615b5a4bedc0f4
|
|
| BLAKE2b-256 |
e3350b9f7f4e168796ea8bb36a72ab25e4c6abcf4074b7f84ad356b1224131a3
|
Provenance
The following attestation bundles were made for apiexception-0.2.2.tar.gz:
Publisher:
python-publish.yml on akutayural/APIException
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
apiexception-0.2.2.tar.gz -
Subject digest:
88d99486d4ea9277bc294958b3bcca004e0490700d9974a36173f84aaa136cba - Sigstore transparency entry: 813840463
- Sigstore integration time:
-
Permalink:
akutayural/APIException@32d2a35d2981cc4f0bb9a9c59a90129c8a3907c3 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/akutayural
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@32d2a35d2981cc4f0bb9a9c59a90129c8a3907c3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file apiexception-0.2.2-py3-none-any.whl.
File metadata
- Download URL: apiexception-0.2.2-py3-none-any.whl
- Upload date:
- Size: 26.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d38b1f9b1146fcc7a2c156e56934214d9dcb1f07caa2fe84a37c81b2c4487a4a
|
|
| MD5 |
20d20127d77bcaa49083be3c811f82ea
|
|
| BLAKE2b-256 |
5def97b0580ba80930766bb893baa811d2fdfaad7aaab679d722d1ba87f37177
|
Provenance
The following attestation bundles were made for apiexception-0.2.2-py3-none-any.whl:
Publisher:
python-publish.yml on akutayural/APIException
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
apiexception-0.2.2-py3-none-any.whl -
Subject digest:
d38b1f9b1146fcc7a2c156e56934214d9dcb1f07caa2fe84a37c81b2c4487a4a - Sigstore transparency entry: 813840466
- Sigstore integration time:
-
Permalink:
akutayural/APIException@32d2a35d2981cc4f0bb9a9c59a90129c8a3907c3 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/akutayural
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@32d2a35d2981cc4f0bb9a9c59a90129c8a3907c3 -
Trigger Event:
release
-
Statement type: