APIException: Standardised Exception Handling for FastAPI¶
β‘ Quick Installation¶
Download the package from PyPI and install it using pip:
pip install apiexception

If you already have Poetry and the uv together, you can install it with:
uv add apiexception
# You can also use the `uv` command to install it:
uv pip install apiexception
# Or, if you prefer using Poetry:
poetry add apiexception
After installation, verify itβs working:
pip show apiexception
Now that you have the package installed, letβs get started with setting up your FastAPI app.
Just import the register_exception_handlers
function from apiexception
and call it with your FastAPI app instance to set up global exception handling:
from api_exception import register_exception_handlers
from fastapi import FastAPI
app = FastAPI()
register_exception_handlers(app=app)
Pro tip
π For advanced configuration, see register_exception_handlers reference
Now all your endpoints will return consistent success
and error
responses, and your Swagger docs will be beautifully documented.
Exception handling will be logged, and unexpected errors will return a clear JSON response instead of FastAPIβs default HTML error page.
π See It in Action!¶
from typing import List
from fastapi import FastAPI, Path
from pydantic import BaseModel, Field
from api_exception import (
APIException,
BaseExceptionCode,
ResponseModel,
register_exception_handlers,
APIResponse
)
app = FastAPI()
# Register exception handlers globally to have the consistent
# error handling and response structure
register_exception_handlers(app=app)
# 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."
)
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 the example above, when the
user_id
is1
, it raises anAPIException
with a customerror_code
, the response is formatted according to theResponseModel
and 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_id
is3
, the program automatically catches theZeroDivisionError
and 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."
}
π‘ Clear & Consistent Responses¶
- π’ 200: Success responses are always documented with your data model.
- π 401 / 403: Custom error codes & messages show exactly what clients should expect.
- π No guesswork β frontend, testers, and API consumers always know what to expect for both success and error cases.
- πͺ Even unexpected server-side issues (DB errors, unhandled exceptions, third-party failures) return a consistent JSON that follows your
ResponseModel
. - β No more raw HTML
500
pages! Every error is logged automatically for an instant audit trail.
π‘ Frontend Integration Advantages¶
In most APIs, the frontend must:
- Check the HTTP status code
- Parse JSON
- Extract data or error details
With APIException, every response follows the same schema:
β Simply parse JSON once and check the status
field (SUCCESS
or ERROR
).
β If ERROR
, read error_code
and message
(or/and description
) for full details. Since even the unexpected errors are formatted consistently, the frontend can handle them uniformly.
Flow | Steps | What the frontend checks |
---|---|---|
Typical REST | 1) Check HTTP status β 2) Parse JSON β 3) Branch for data/error | Status code, JSON shape, error payload variations |
With APIException | 1) Parse JSON once | Read status β SUCCESS or ERROR |
Client pattern:
// Example: fetch wrapper / interceptor
const res = await fetch(url, opts);
const body = await res.json(); // same shape for 2xx/4xx/5xx
if (body.status === "SUCCESS") {
return body.data; // β
consume data directly
} else {
throw { code: body.error_code, message: body.description }; // β unified error
}
π‘ Backend Maintainability Advantages¶
- Define each
CustomExceptionCode
once witherror_code
,message
, anddescription
. - Logs become cleaner and easier to search.
β If another team reports anerror_code
, you can instantly locate it in logs. - Keeps backend code organized and avoids scattering error definitions everywhere.
- Share the
error_code
list with frontend teams for zero-guesswork integrations.
π Logging & Debugging Flexibility¶
- Toggle tracebacks on/off depending on the environment.
- Fully controllable logging: import, set log levels, or disable entirely.
- RFC 7808 support out of the box for teams that require standard-compliant error formats.
Pro Tip: Master Your Logs
For advanced logging configuration and real-world examples, check out the
Logging & Debug Guide.
Learn how to add file handlers, mask sensitive data, and keep production logs clean & actionable.
Reduces boilerplate and speeds up integration. This is how APIException helps you build trustable, professional APIs from day one!
π₯ Who should use this?¶
β
FastAPI developers who want consistent success & error responses.
β
Teams building multi-client or external APIs.
β
Projects where Swagger/OpenAPI docs must be clear and human-friendly.
β
Teams that need extensible error code management.
If youβre tired of:
-
Inconsistent response structures,
-
Confusing Swagger docs,
-
Messy exception handling,
-
Finding yourself while trying to find the exception that isn't logged
-
Backend teams asking βWhat does this endpoint return?β,
-
Frontend teams asking βWhat does this endpoint return in error?β,
then this library is for you.
π― Why did I build this?¶
After 4+ years as a FastAPI backend engineer, Iβve seen how crucial a clean, predictable response model is.
When your API serves multiple frontends or external clients, having different JSON shapes, missing status info, or undocumented error codes turns maintenance into chaos.
So, this library:
β
Standardizes all success & error responses,
β
Documents them beautifully in Swagger,
β
Provides a robust ExceptionCode pattern,
β
Adds an optional global fallback for unexpected crashes β all while keeping FastAPIβs speed.
β¨ Core Principles¶
Principle | Description |
---|---|
π Consistency | Success and error responses share the exact same structure, improving reliability and DX. |
π Clear Docs | OpenAPI/Swagger remains clean, accurate, and human-friendly. |
πͺΆ Zero Boilerplate | Configure once, then use anywhere with minimal repetitive code. |
β‘ Extensible | Fully customizable error codes, handlers, and response formats for any project need. |
π Benchmark¶
We benchmarked apiexception's APIException
against FastAPI's built-in HTTPException
using Locust with 200 concurrent users over 2 minutes.
Both apps received the same traffic mix (β75% /ok
, β25% /error
).
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:
APIException
automatically logs exceptions, while FastAPIβs built-inHTTPException
does 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.
π Next Steps¶
Ready to integrate? Check out: - π Installation β How to set up APIException.
-
β‘ Quick Start β Add it to your project in minutes.
-
π§© Usage β Response models, custom codes, and fallback middleware.
-
π Advanced β Swagger integration, logging, debugging.
-
π API Reference β Full reference docs.