📚

# 3.3 エラーハンドリングの実装パターン(Django REST Framework 編)

に公開

API 設計において エラーハンドリングの統一 は非常に重要。
レスポンス形式がバラバラだとフロント側での処理が複雑になり、ユーザー体験も悪化する。

この記事では、Django REST Framework(DRF)を使ったエラーハンドリングの実装パターンを整理する。


1. バリデーションエラー

Serializer の validate_*validate を使うと、入力チェックでエラーを返せる。

class OrderInputSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["customer_name", "total_amount"]

    def validate_total_amount(self, value):
        if value <= 0:
            raise serializers.ValidationError("金額は 0 より大きい必要があります。")
        return value

リクエスト例:

{ "customer_name": "株式会社テスト", "total_amount": -100 }

レスポンス例(400 Bad Request):

{
  "error": "ValidationError",
  "message": {
    "total_amount": ["金額は 0 より大きい必要があります。"]
  }
}

2. 認証・権限エラー

ログインしていない場合や権限不足のときは DRF が自動で 401 / 403 を返す。
これもフォーマットを統一して返したい。

レスポンス例(403 Forbidden):

{
  "error": "PermissionDenied",
  "message": "この操作を実行する権限がありません。"
}

3. 業務ロジックエラー

業務システム特有の「状態による制御」もエラーとして扱う必要がある。

例: 「承認済みの受注は修正できない」

from rest_framework.exceptions import APIException

class BusinessLogicError(APIException):
    status_code = 409
    default_detail = "業務ロジックエラー"
    default_code = "business_error"

def update_order(order, user):
    if order.status == "APPROVED":
        raise BusinessLogicError("承認済みの受注は修正できません")

レスポンス例(409 Conflict):

{
  "error": "BusinessLogicError",
  "message": "承認済みの受注は修正できません"
}

4. 想定外エラーの統一

サーバー側の例外(例: DB エラー)をそのまま返すのは危険。
共通の例外ハンドラを定義して、返却形式を統一する。

# utils/exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if response is not None:
        # DRF 標準のエラーをラップ
        return Response({
            "error": exc.__class__.__name__,
            "message": response.data
        }, status=response.status_code)

    # 想定外のエラー
    return Response({
        "error": "ServerError",
        "message": "予期しないエラーが発生しました。"
    }, status=500)

設定に組み込む:

# settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "core.utils.exceptions.custom_exception_handler"
}

5. 共通レスポンス形式を決める

プロジェクトではあらかじめ「エラーはこの形式で返す」とルールを決めておくと良い。

例:

{
  "error": "ValidationError",
  "message": {
    "field": ["エラーメッセージ"]
  }
}
{
  "error": "PermissionDenied",
  "message": "この操作を実行する権限がありません。"
}
{
  "error": "BusinessLogicError",
  "message": "承認済みの受注は修正できません"
}
{
  "error": "ServerError",
  "message": "予期しないエラーが発生しました。"
}

6. フロント側での扱いやすさを意識する

フロントエンドでは次のように使いやすくなる。

  • error → エラー種別で分岐(ValidationError, PermissionDenied, BusinessLogicError など)
  • message → そのまま画面に表示できる

Vue 側のサンプル:

try {
  await axios.post("/api/orders/", payload);
} catch (err) {
  const error = err.response.data;
  if (error.error === "ValidationError") {
    showFormErrors(error.message);
  } else {
    alert(error.message);
  }
}

さらに、axios の interceptor を使って共通化すると便利。

import axios from "axios";
import { showErrorDialog } from "@/components/core/ErrorDialog";

const api = axios.create({ baseURL: "/api" });

api.interceptors.response.use(
  res => res,
  err => {
    const error = err.response?.data;
    if (error) {
      showErrorDialog(error.message);
    } else {
      showErrorDialog("予期しないエラーが発生しました");
    }
    return Promise.reject(err);
  }
);

export default api;

これにより、全ての API エラーを統一的にハンドリングできる。


API 設計において エラーハンドリングの統一 は非常に重要。
レスポンス形式がバラバラだとフロント側での処理が複雑になり、ユーザー体験も悪化する。

この記事では、Django REST Framework(DRF)を使ったエラーハンドリングの実装パターンを整理する。


1. バリデーションエラー

Serializer の validate_*validate を使うと、入力チェックでエラーを返せる。

class OrderInputSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["customer_name", "total_amount"]

    def validate_total_amount(self, value):
        if value <= 0:
            raise serializers.ValidationError("金額は 0 より大きい必要があります。")
        return value

リクエスト例:

{ "customer_name": "株式会社テスト", "total_amount": -100 }

レスポンス例(400 Bad Request):

{
  "error": "ValidationError",
  "message": {
    "total_amount": ["金額は 0 より大きい必要があります。"]
  }
}

2. 認証・権限エラー

ログインしていない場合や権限不足のときは DRF が自動で 401 / 403 を返す。
これもフォーマットを統一して返したい。

レスポンス例(403 Forbidden):

{
  "error": "PermissionDenied",
  "message": "この操作を実行する権限がありません。"
}

3. 業務ロジックエラー

業務システム特有の「状態による制御」もエラーとして扱う必要がある。

例: 「承認済みの受注は修正できない」

from rest_framework.exceptions import APIException

class BusinessLogicError(APIException):
    status_code = 409
    default_detail = "業務ロジックエラー"
    default_code = "business_error"

def update_order(order, user):
    if order.status == "APPROVED":
        raise BusinessLogicError("承認済みの受注は修正できません")

レスポンス例(409 Conflict):

{
  "error": "BusinessLogicError",
  "message": "承認済みの受注は修正できません"
}

4. 想定外エラーの統一

サーバー側の例外(例: DB エラー)をそのまま返すのは危険。
共通の例外ハンドラを定義して、返却形式を統一する。

# utils/exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if response is not None:
        # DRF 標準のエラーをラップ
        return Response({
            "error": exc.__class__.__name__,
            "message": response.data
        }, status=response.status_code)

    # 想定外のエラー
    return Response({
        "error": "ServerError",
        "message": "予期しないエラーが発生しました。"
    }, status=500)

設定に組み込む:

# settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "core.utils.exceptions.custom_exception_handler"
}

5. 共通レスポンス形式を決める

プロジェクトではあらかじめ「エラーはこの形式で返す」とルールを決めておくと良い。

例:

{
  "error": "ValidationError",
  "message": {
    "field": ["エラーメッセージ"]
  }
}
{
  "error": "PermissionDenied",
  "message": "この操作を実行する権限がありません。"
}
{
  "error": "BusinessLogicError",
  "message": "承認済みの受注は修正できません"
}
{
  "error": "ServerError",
  "message": "予期しないエラーが発生しました。"
}

6. フロント側での扱いやすさを意識する

フロントエンドでは次のように使いやすくなる。

  • error → エラー種別で分岐(ValidationError, PermissionDenied, BusinessLogicError など)
  • message → そのまま画面に表示できる

Vue 側のサンプル:

try {
  await axios.post("/api/orders/", payload);
} catch (err) {
  const error = err.response.data;
  if (error.error === "ValidationError") {
    showFormErrors(error.message);
  } else {
    alert(error.message);
  }
}

さらに、axios の interceptor を使って共通化すると便利。

import axios from "axios";
import { showErrorDialog } from "@/components/core/ErrorDialog";

const api = axios.create({ baseURL: "/api" });

api.interceptors.response.use(
  res => res,
  err => {
    const error = err.response?.data;
    if (error) {
      showErrorDialog(error.message);
    } else {
      showErrorDialog("予期しないエラーが発生しました");
    }
    return Promise.reject(err);
  }
);

export default api;

これにより、全ての API エラーを統一的にハンドリングできる。


AI活用のポイント

エラーハンドリングのコードは、AIに生成させるのが特に相性が良い領域。
ただし、使い方にはちょっとしたコツがある。

  • 最初はおまかせで依頼する
    「エラーハンドリングを統一したいからコードを出して」とざっくり指示すると、雛形はすぐ出てくる。

  • 既存コードを提示して「これと同じように」と依頼する
    プロジェクト独自の書き方がある場合は、過去のコードを渡して合わせてもらうのが一番早い。

  • うまくいかない場合だけ細かく指示する
    返却フォーマットやステータスコードの扱いなど、AIが迷いやすい部分は必要に応じて個別に補足する。

このように「まずはAIに任せる → 必要に応じて修正指示」という流れにすることで、余計な手間をかけずにスピーディに仕組みを整えられる。


まとめ

DRF でのエラーハンドリングは以下を押さえるのがポイント。

  • Serializer → バリデーションエラー
  • 認証・権限エラー → 401 / 403
  • 業務ロジックエラー → 409 / 422
  • 想定外エラー → 共通ハンドラでラップ
  • 返却形式を統一してフロントで処理しやすくする
  • Vue 側では interceptor を使って共通的に扱う

そして AI活用 では:

  • まずはおまかせでコードを生成させる
  • 必要なら既存コードを例示して「同じ形式で」と依頼する
  • どうしても揃わない部分だけ個別に指示する

このシンプルな使い方で、AIを活用しつつ、プロジェクト固有のルールに合わせたエラーハンドリングを実現できる。


次回は フィルタリングと検索の実装パターン を紹介する予定。

Discussion