APIException for FastAPI

repository·main·Indexed 42 days ago

https://github.com/akutayural/apiexception

A library for FastAPI version 0.2.3 that standardizes JSON response formatting and exception handling. It provides consistent structures for success and error cases, automatic structured logging, and global error handling. It supports RFC 7807 (application/problem+json) for standardized error responses and integrates with OpenAPI/Swagger to document response schemas.

Tokens
14.3K
Snippets
51
Records
59
Agent score
53%

What's inside APIException

  1. Standardize API responses with `ResponseModel`

    main

    Use ResponseModel to ensure that every API response (both successful and error states) follows a consistent JSON structure. This makes your API predictable for frontend consumers and simplifies OpenAPI/Swagger documentation.

    Every response generated via ResponseModel includes the following fields:

    • data: The actual payload (populated on success, null on failure).
    • status: An ExceptionStatus value (e.g., SUCCESS, WARNING, FAIL).
    • message: A short summary of the outcome.
    • error_code: A specific error identifier (populated only if there is an error).
    • description: Extra context for debugging.

    Interpretation Logic:

    • Success: data is populated and error_code is null.
    • Failure: error_code is populated.
    • Frontend Logic: Use the status field as the primary driver for application logic.
    # Example of the resulting JSON structure
    {
      "data": { "api_key": "valid_key" },
      "status": "SUCCESS",
      "message": "API key is valid",
      "error_code": null,
      "description": "The provided API key is valid."
    }
  2. Define custom business-specific error codes

    main

    To maintain a predictable and self-documenting API, you can define custom error codes by extending BaseExceptionCode. This allows you to organize unique, human-readable codes that clients can easily use to handle specific failure scenarios.

    Each error code follows a specific tuple format: KEY = (code, message, description, rfc7807_type, rfc7807_instance)

    • code (Required): The unique string identifier (e.g., "USR-404").
    • message (Required): A short, human-readable error message.
    • description (Optional): A detailed explanation of the error.
    • rfc7807_type (Optional): The RFC 7807 error type.
    • rfc7807_instance (Optional): The RFC 7807 error instance.
    from api_exception import BaseExceptionCode
    
    class CustomExceptionCode(BaseExceptionCode):
        # Format: KEY = (code, message, description, rfc7807_type, rfc7807_instance)
        #               # Req, # Req,   # Optional,  # Optional,   # Optional
        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.")
  3. Understand the standardized JSON response schema

    main

    APIException enforces a consistent JSON structure for both successful and failed requests, which simplifies frontend integration.

    Success Response Structure

    {
      "data": { ... },
      "status": "SUCCESS",
      "message": "Operation completed successfully.",
      "error_code": null,
      "description": "User found."
    }

    Error Response Structure (Custom Exception)

    {
      "data": null,
      "status": "FAIL",
      "message": "User not found.",
      "error_code": "USR-404",
      "description": "The user ID does not exist."
    }

    Error Response Structure (Uncaught/Unexpected Exception)

    {
      "data": null,
      "status": "FAIL",
      "message": "Something went wrong.",
      "error_code": "ISE-500",
      "description": "An unexpected error occurred. Please try again later."
    }
  4. Choose a response format for errors

    main

    You can control how error responses are serialized using the response_format parameter. The available options are:

    • ResponseFormat.RESPONSE_MODEL: Uses your internal ResponseModel schema. This is the default and provides a consistent shape for product APIs.
    • ResponseFormat.RFC7807: Returns RFC 7807 Problem Details (application/problem+json). Best for public APIs and standards-driven clients.
    • ResponseFormat.RESPONSE_DICTIONARY: Returns plain dictionaries without models. Best for ultra-lightweight responses or prototypes.
    from api_exception import register_exception_handlers, ResponseFormat
    
    # Example: Using RFC 7807
    register_exception_handlers(
        app,
        response_format=ResponseFormat.RFC7807,
        include_null_data_field_in_openapi=False
    )
  5. Quickstart: Integrate APIException with FastAPI

    main

    To use APIException, follow these three steps:

    1. Register Handlers: Call register_exception_handlers(app) on your FastAPI instance. By default, it uses ResponseModel for formatting.
    2. Define Custom Error Codes: Extend BaseExceptionCode to create your own domain-specific error codes.
    3. Raise Exceptions: Use raise APIException(...) within your route handlers to trigger standardized error responses.

    For successful responses, wrap your data in a ResponseModel to ensure the JSON structure remains consistent with your error responses.

    from fastapi import FastAPI
    from pydantic import BaseModel, Field
    from api_exception import (
        register_exception_handlers,
        APIException,
        BaseExceptionCode,
        ResponseModel,
        APIResponse,
    )
    
    app = FastAPI()
    register_exception_handlers(app)
    
    class CustomExceptionCode(BaseExceptionCode):
        USER_NOT_FOUND = ("USR-404", "User not found.", "The user ID does not exist.")
    
    class UserModel(BaseModel):
        id: int = Field(..., example=1)
        username: str = Field(..., example="John Doe")
    
    @app.get(
        "/user/{user_id}",
        response_model=ResponseModel[UserModel],
        responses=APIResponse.default(),
    )
    async def user(user_id: int):
        if user_id == 1:
            raise APIException(
                error_code=CustomExceptionCode.USER_NOT_FOUND,
                http_status_code=404,
            )
        
        return ResponseModel[UserModel](
            data=UserModel(id=user_id, username="John Doe"),
            description="User retrieved successfully.",
        )
  6. Integrate APIException with FastAPI

    main

    To enable global standardized exception handling and consistent response structures in your FastAPI application, import register_exception_handlers and call it with your FastAPI app instance during startup.

    This setup ensures:

    • All endpoints return consistent success and error JSON responses.
    • Swagger/OpenAPI documentation is automatically updated with predictable response schemas.
    • Unexpected server errors (like ZeroDivisionError) are caught and returned as clean JSON instead of HTML error pages.
    • Exceptions are automatically logged.
    from api_exception import register_exception_handlers
    from fastapi import FastAPI
    
    app = FastAPI()
    register_exception_handlers(app=app)
  7. Support multiple FastAPI applications

    main

    You can use register_exception_handlers to apply standardized exception handling to multiple independent FastAPI instances within the same project. This ensures consistent error responses across different app modules (e.g., mobile, admin, and merchant apps).

    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)
  8. Install APIException

    main

    You can install apiexception using pip, uv, or poetry.

    Using pip:

    pip install apiexception

    Using uv:

    uv add apiexception
    # or
    uv pip install apiexception

    Using Poetry:

    poetry add apiexception

    After installation, verify the package is present using:

    pip show apiexception
    pip install apiexception
  9. Enable RFC 7807 globally in FastAPI

    main

    To adopt the IETF standard for consistent error responses using the application/problem+json media type, use register_exception_handlers with the ResponseFormat.RFC7807 argument. This ensures all error responses follow the structured RFC 7807 format containing type, title, status, detail, and instance fields.

    from api_exception import register_exception_handlers, ResponseFormat
    from fastapi import FastAPI
    
    app = FastAPI()
    register_exception_handlers(
        app, 
        response_format=ResponseFormat.RFC7807
    )
  10. Implement standardized error handling with APIException

    main

    To use apiexception in an endpoint, follow these steps:

    1. Define a Response Model: Use Pydantic's BaseModel to define the structure of your successful data.
    2. Define Custom Exception Codes: Extend BaseExceptionCode to create domain-specific error codes. Each code is a tuple containing (error_code_string, short_message, detailed_description).
    3. Configure the Endpoint:
      • Use ResponseModel[YourModel] as the response_model in the decorator.
      • Use APIResponse.default() in the responses parameter of the decorator to populate Swagger/OpenAPI documentation.
    4. Raise Exceptions: Use raise APIException(...) with your custom code and an optional http_status_code to trigger a standardized error response.
    from fastapi import FastAPI, Path
    from pydantic import BaseModel, Field
    from api_exception import APIException, register_exception_handlers, ResponseModel, APIResponse, BaseExceptionCode
    
    app = FastAPI()
    
    # 1. Register handlers
    register_exception_handlers(app=app)
    
    # 2. Create validation model
    class UserResponse(BaseModel):
        id: int = Field(..., example=1, description="Unique identifier of the user")
        username: str = Field(..., example="Micheal Alice", description="Username or full name of the user")
    
    # 3. Define custom exception codes
    class CustomExceptionCode(BaseExceptionCode):
        USER_NOT_FOUND = ("USR-404", "User not found.", "The user ID does not exist.")
    
    @app.get("/user/{user_id}",
        response_model=ResponseModel[UserResponse],
        responses=APIResponse.default()
    )
    async def user(user_id: int = Path()):
        if user_id == 1:
            # 4. Raise standardized exception
            raise APIException(
                error_code=CustomExceptionCode.USER_NOT_FOUND,
                http_status_code=401,
            )
        
        data = UserResponse(id=1, username="John Doe")
        return ResponseModel[UserResponse](
            data=data,
            description="User found and returned."
        )