Django Rest Framework exposes a neat hook to customize the response payload of your API when errors occur. I was going through Microsoft’s REST API guideline1 and wanted to make the error response of my APIs more uniform and somewhat similar to this2.

I’ll use a modified version of the quickstart example3 in the DRF docs to show how to achieve that. Also, we’ll need a POST API to demonstrate the changes better. Here’s the same example with the added POST API. Place this code in the project’s file.


from django.urls import path, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets

# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ["url", "username", "email", "is_staff"]

    def validate_username(self, username: str) -> str:
        if len(username) < 10:
            raise serializers.ValidationError(
                "Username must be at least 10 characters long.",
        return username

    def validate_email(self, email: str) -> str:
        except ValidationError:
            raise serializers.ValidationError("Invalid email format.")
        return email

    def create(self, validated_data: str) -> User:
        return User.objects.create(**validated_data)

# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r"users", UserViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    path("", include(router.urls)),
        "api-auth/", include("rest_framework.urls", namespace="rest_framework")

If you make a POST request to /users endpoint with the following payload where it’ll intentionally fail email and username validation—

  "username": "hello",
  "email": "email..",
  "is_staff": false

you’ll see the following response:

    "Username must be at least 10 characters long."
    "Enter a valid email address."

While this is okay, there’s one gotcha here. The error payload isn’t consistent. Depending on the type of error, the shape of the response payload will change. This can be a problem if your system has custom error handling logic that expects a consistent response.

I wanted the error payload to have a predictable shape while carrying more information like—HTTP error code, error message, etc. You can do it by wrapping the default rest_framework.views.exception_handler function in a custom exception handler function. Let’s write the api_exception_handler:

from rest_framework.views import exception_handler
from http import HTTPStatus
from typing import Any

from rest_framework.views import Response


def api_exception_handler(exc: Exception, context: dict[str, Any]) -> Response:
    """Custom API exception handler."""

    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)

    if response is not None:
        # Using the description's of the HTTPStatus class as error message.
        http_code_to_message = {v.value: v.description for v in HTTPStatus}

        error_payload = {
            "error": {
                "status_code": 0,
                "message": "",
                "details": [],
        error = error_payload["error"]
        status_code = response.status_code

        error["status_code"] = status_code
        error["message"] = http_code_to_message[status_code]
        error["details"] = = error_payload
    return response


Now, you’ll have to register this custom exception handler in the file. Head over to the REST_FRAMEWORK section and add the following key:

    "EXCEPTION_HANDLER": "<project>.urls.api_exception_handler",

If you make a POST request to /users endpoint with an invalid payload as before, you’ll see this:

  "error": {
    "message":"Bad request syntax or unsupported method",
        "Username must be at least 10 character long."
        "Enter a valid email address."

Much nicer!

Recent posts

  • Protobuffed contracts
  • TypeIs does what I thought TypeGuard would do in Python
  • ETag and HTTP caching
  • Crossing the CORS crossroad
  • Dysfunctional options pattern in Go
  • Einstellung effect
  • Strategy pattern in Go
  • Anemic stack traces in Go
  • Retry function in Go
  • Type assertion vs type switches in Go