コンテンツにスキップ

HTTPレスポンス総論#

2024 iThome鉄人レース

これはDjango Ninjaシリーズチュートリアルの第13回です。

この記事から本格的に「HTTPレスポンス」の段階、すなわち第3節に入ります。

本節では4つの記事を通じて、Django NinjaがどのようにHTTPレスポンスを処理するかを紹介します:

より多くのスキーマの使い方を解説し、これらのテクニックを通じてAPIの出力フォーマットを正確に制御できるようになります。単一オブジェクトのレスポンスから複雑なネスト構造まで、順次説明していきます。

この記事のすべてのコードの変更は、こちらのPRを参考にしてください。

GitHubサンプルプロジェクト#

👉 Django-Ninja-Tutorial


この記事では、簡単なものから複雑なものへ、段階的にDjango Ninjaを通じてHTTPレスポンスを作成する方法を紹介します。

また、既存の3つのAPIを使用してデモンストレーションを行います(必要に応じて様々な内容を追加します):

  1. 記事の追加:シンプルなレスポンスのデモ、ステータスコードを追加します。
  2. 単一記事の取得:単一オブジェクトのレスポンスのデモ、スキーマとresponse=パラメータの定義が必要です。
  3. 記事リストの取得:複数オブジェクトのレスポンスのデモ。

始めましょう!

1. シンプルなレスポンス:記事の追加#

まずは最もシンプルなレスポンス形式を見てみましょう。この例では、Pythonの辞書を返す方法と、HTTPレスポンスのステータスコードを手動で設定する方法を示します。

「記事の追加」APIを例とします:(一部のコードは省略)

@router.post(path='/posts/')
def create_post(...) -> dict:
    ...
    return {'id': post.id, 'title': post.title}

ここで返しているのはPythonの辞書です。実際には、「JSONにシリアライズ可能なあらゆるもの」をPythonデータとしてreturnできます。(そのため、Djangoモデルオブジェクトは不可です。直接シリアライズできないためです)

したがって、以下のものはすべてreturn可能です:

  • 単純な文字列:"Hello World !"
  • Pythonのlist:[1 , 2 , 3]
  • ネストされたデータ構造:{"name": "Alice", "age": 30, "hobbies": ["reading", "swimming"]}

これらは自動的にDjango NinjaによってJSON形式にシリアライズされ、APIのレスポンスとして機能します:

{
    "id": 666,
    "title": "How to Be a Ninja"
}

レスポンスにHTTPステータスコードを追加する#

view関数でレスポンスを処理する際、しばしばHTTPステータスコードを追加する必要があります。特に複数のレスポンス状態がある場合、ステータスコードを通じてそれらを区別する必要があります。

方法は非常に簡単で、レスポンス内容の前に直接追加するだけです:

return 201, {'id': post.id, 'title': post.title}

これにより、関数の戻り値の型は元のdictからtupleに変わります。

そのため、関数シグネチャの型ヒントも合わせて修正する必要があります:

def create_post(...) -> tuple[int, dict]:

この前方のステータスコードの数値を指定しない場合、Django Ninjaはそれをデフォルトで200とします。

注意すべき点として、view関数が「200以外」のレスポンスをreturnする場合、routerデコレータで宣言する必要があります:

@router.post(path='/posts/', response={201: dict})  # ここ

response={201: dict}がその宣言方法であり、Pythonの辞書を用いてステータスコード返却内容の形式を1対1で対応させます。

執筆当時、この部分のサンプルプロジェクトのコードがまだ補完されていなかったため、このAPIは正常に応答できませんでした😅、ここでお知らせしておきます。


上記の1つ目のレスポンスは非常にシンプルですが、ほとんどのAPIレスポンスはそこまで単純ではありません

2つ目のレスポンスを見てみましょう。

2. 単一モデルオブジェクトのレスポンス:単一記事の取得#

Django APIを開発する際、レスポンスデータの大部分はDjangoモデルオブジェクトからシリアライズされたものです。

しかし通常、データベース内のすべての情報をフロントエンドに直接送信することはありません。逆に、フィールドのフィルタリング、検証、またはフォーマット変換を行います。

これにより、APIの出力を正確に制御できるだけでなく、データの正確性と安全性を確保できます。

Django Ninjaでは、これらの「フィルタリング、検証、フォーマット変換」などの要件は、すべてスキーマを通じて実現されます。

「単一記事の取得」APIのレスポンス形式を、スキーマを使用して設計してみましょう。

# post/schemas.py
from datetime import datetime
...

class PostResponse(Schema):
    id: int
    title: str
    content: str
    author_id: int
    created_at: datetime
    updated_at: datetime

このPostResponseスキーマには、Postのほぼすべてのフィールドが含まれています。

注意として、スキーマの定義が出力されるフィールドを決定します。スキーマの中にidという列しか定義されていなければ、出力結果にはその列のデータしか含まれません。

次に、view関数でこのスキーマを使用します:

@router.get(path='/posts/{int:post_id}/', response=PostResponse)
def get_post(request: HttpRequest, post_id: int) -> Post:
    """
    単一記事の取得
    """
    post = Post.objects.get(id=post_id)
    return post

変更したのは1行だけです!——routerデコレータにresponse=PostResponseを追加しました。

response=PostResponseの設定により、Django Ninjaは関数が返すPostモデルオブジェクトをPostResponseに渡して検証し、成功した後に直接JSON形式に変換してフロントエンドに送信します。

レスポンス結果を見てみましょう:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author_id": 1,
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

素晴らしいですね!


3. 複数モデルオブジェクトのレスポンス:記事リストの取得#

リスト、一覧」もAPIの一般的なレスポンス形態であり、複数のデータを含みます。

先ほどのPostResponseを引き続き使用し、一切変更を加えずにそのまま「記事リストの取得」APIに適用してみましょう。

同様に1行変更するだけですが、先ほどとは少し異なります

@router.get(path='/posts/', response=list[PostResponse])

list[PostResponse]を使用しました。これはレスポンスがPostResponseオブジェクトのリストになることを示しています。

Django NinjaによるIterableの自動処理#

しかし実際のところ、この時Pythonのリストを「本当に」returnする必要はありません直接QuerySetを返すだけで十分です。Django Ninjaがオブジェクトの反復処理とシリアライズを自動的に処理してくれます。

さらに言えば、returnするものがiterableであり、そのiterable内の各要素がPostResponseの検証を通過できる(形式に合致している)のであれば、それで十分なのです!

結果を見てみましょう。リストが長すぎるため、スクリーンショットで表示します:

APIレスポンス:記事リストの取得


複数ステータスコードのレスポンス#

上述したレスポンスは200または201のいずれかでしたが、通常APIには400、401、403、さらには500などのレスポンスも存在します。これらの対応関係をどのように処理すればよいのでしょうか?

そうです、response=の中の辞書を拡張するのです!公式ドキュメントの例を直接見てみましょう:

class Token(Schema):
    token: str
    expires: date

class Message(Schema):
    message: str

@api.post(
    path='/login',
    response={200: Token, 401: Message, 402: Message}
)
...

注目すべきは、辞書のキーは重複できませんが、値は重複可能であるということです!——Message2回出現しています。

しかし、この「複数ステータスコードのレスポンス」設定は、実務においてそれほど実用的ではないと私は考えています。なぜでしょうか?これについては後ほど説明します。


まとめ#

この記事では、最もシンプルなレスポンスから始め、レスポンスで単一データおよび複数データを返す方法を段階的に紹介し、Django Ninjaが複数ステータスコードのレスポンスをどのように設定するかについても触れました。

次の記事では、レスポンスにおける複雑なネスト構造を処理し、私たちのAPIをさらに堅牢なものにする方法を探求します。