Django REST framework（DRF）公式チュートリアル② Requests and Responses

# Tutorial 2: Requests and Responses

### はじめに
本記事は、Django REST framework の公式チュートリアル[Tutorial 2: Requests and Responses](https://www.django-rest-framework.org/tutorial/2-requests-and-responses/)の内容をもとに、和訳および補足解説を行ったものです。

基本的にはチュートリアルの翻訳を軸に構成しており、`✏️補足`の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。

他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇  
[DRF（Django REST framework）公式チュートリアルを日本語でまとめてみた【全6回】](https://qiita.com/yutty-kov/items/1850f3e865ec1a50a7bb)

それでは、本チュートリアルを始めましょう。
- - -
ここからは、REST framework の中核となる部分に本格的に踏み込んでいきます。まずは、いくつかの基本的な構成要素を紹介しましょう。

### リクエストオブジェクト

REST framework では、通常の <font color="#d14">`HttpRequest`</font> を拡張した <font color="#d14">`Request`</font> オブジェクトが導入されており、より柔軟なリクエストのパース（構文解析）を可能にしています。
この <font color="#d14">`Request`</font> オブジェクトの中核となる機能は <font color="#d14">`request.data`</font> 属性であり、<font color="#d14">`request.POST`</font> に似ていますが、Web API を扱う上でより実用的です。

```python:
request.POST # フォームデータのみを扱います。'POST' メソッドでのみ機能します。
request.data # 任意のデータを扱えます。'POST'、'PUT'、'PATCH' メソッドで機能します。
```

### レスポンスオブジェクト
REST framework はまた、<font color="#d14">`Response`</font> オブジェクトも導入しています。これは <font color="#d14">`TemplateResponse`</font> の一種で、未レンダリングのコンテンツを受け取り、コンテンツネゴシエーションによって、クライアントに返すべき適切なコンテンツタイプを判断します。

```python:
return Response(data)  # クライアントの要求に応じたコンテンツタイプでレンダリングされます。
```

### ステータスコード
View内で数値のHTTPステータスコードを使っても、必ずしも可読性が高いとは言えず、間違ったエラーコードを使ってしまっても気づきにくいことがあります。
REST frameworkでは、<font color="#d14">`HTTP_400_BAD_REQUEST`</font> のように、各ステータスコードに対してより明示的な識別子を <font color="#d14">`status`</font> モジュール内で提供しています。
数値の識別子を使うのではなく、これらの識別子を使用することをおすすめします。

### APIビューをラップする
REST framework では、APIビューを記述するためのラッパーが2つ用意されています。
1. 関数ベースビューに使用できる <font color="#d14">`@api_view`</font> デコレーター
2. クラスベースビューに使用できる <font color="#d14">`APIView`</font> クラス

これらのラッパーは、Viewで <font color="#d14">`Request`</font> インスタンスを受け取れるようにすることや、<font color="#d14">`Response`</font>オブジェクトにコンテキストを追加してコンテンツネゴシエーション（内容の自動判別）を可能にする、といったいくつかの機能を提供しています。

これらのラッパーは、適切なタイミングで <font color="#d14">`405 Method Not Allowed`</font> のレスポンスを返すといった動作や、不正な入力により <font color="#d14">`request.data`</font> にアクセスした際に発生する <font color="#d14">`ParseError`</font> 例外の処理なども提供しています。

### すべてをまとめる
それでは、これらの新しいコンポーネントを使って、Viewを少しリファクタリングしていきましょう。

```python:snippets/views.py
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer


@api_view(['GET', 'POST'])
def snippet_list(request):
    """
    Snippetの一覧を取得、もしくは新しいSnippetを作成する。
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```
今回のインスタンスビューは、直前のサンプルコードよりも改善されたものです。少しだけ簡潔になり、コードはまるで Forms API を使っているときのような感覚に近づきました。また、名前付きのステータスコードを使っているため、レスポンスの意味もより明確になっています。

こちらは <font color="#d14">`views.py`</font> モジュールにある、個別のSnippet用の View です。
```python:snippets/views.py
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
    """
    Snippetを取得・更新・削除する。
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
```

この処理は、通常のDjangoのビューとそれほど違いがないため、とても馴染み深く感じられるはずです。

リクエストやレスポンスを特定のコンテンツタイプに明示的に結びつけていないことに注目してください。
<font color="#d14">`request.data`</font> は受信した<font color="#d14">`JSON`</font> リクエストを扱えますが、他の形式にも対応しています。
同様に、レスポンスオブジェクトにはデータを渡していますが、実際のレスポンスのレンダリングはREST frameworkが適切なコンテンツタイプに変換して処理してくれます。

## ✏️補足
`@api_view` デコレーターが `Request` オブジェクトを生成し、ラップされた `snippet_list()`関数 や `snippet_detail()` 関数で使用できるようになるまでの内部処理を説明します。（今回は`snippet_detail()` 関数を例に解説します）

以下の `@api_view(['GET', 'PUT', 'DELETE'])` は `api_view(['GET', 'PUT', 'DELETE'])(snippet_detail)` と同義で、`api_view()` 関数が呼ばれます。

```python:
### 
@api_view(['GET', 'PUT', 'DELETE']) # api_view(['GET', 'PUT', 'DELETE'])(snippet_detail)
def snippet_detail(request, pk):
    ...
```

このとき、内部的には以下のような処理が行われています：

```python:
def api_view(http_method_names):
    def decorator(func):
        class WrappedAPIView(APIView):
            def dispatch(self, request, *args, **kwargs):
                request = self.initialize_request(request, *args, **kwargs)
                ...
                return func(request, *args, **kwargs)

        return csrf_exempt(WrappedAPIView.as_view())
    return decorator
```

この内部処理の主な流れとしては、以下になります。

1. `api_view()`関数に `['GET', 'PUT', 'DELETE']` が引数として渡される

2. `decorator(func)` の `func`引数 に `snippet_detail()` 関数が渡される

3. `WrappedAPIView.as_view()`関数 を通して、`dispatch()` メソッド が呼ばれる

4. `dispatch` メソッド内で `initialize_request()`メソッド により Django の `HttpRequest` オブジェクトが DRF の `Request` オブジェクトに変換される

5. 変換された `Request` オブジェクトが `func(request, *args, **kwargs)` (`snippet_detail` 関数)に渡される。

6. `csrf_exempt()`関数 によって CSRF チェックを無効化する。

7. `urlpatterns` に登録され、URLがマッチするとラップ済みの関数が呼ばれ、最終的に `snippet_detail` 関数が実行される。

また、最終的に `snippet_detail()` 関数では以下のようにして request（DRFの Request オブジェクト）を使用できます：

```python:
def snippet_detail(request, pk):
    if request.method == 'GET':
        ...
```

`request`（Requestオブジェクト）の主な属性として以下があります：
```python:
request.method        # 'GET', 'POST' など
request.data          # リクエストボディ（JSONなど）
request.query_params  # URLのクエリパラメータ（GETパラメータ）
request.user          # 認証ユーザー
request.auth          # 認証情報（トークンなど）
```

- - -

### URL にオプションのフォーマット接尾辞を追加する
レスポンスが特定のコンテンツタイプに固定されなくなった利点を活かし、フォーマット接尾辞のサポートをAPIエンドポイントに追加しましょう。フォーマット接尾辞を使うことで、<font color="#d14">`http://example.com/api/items/4.json`</font> のような特定のフォーマットを示すURLを扱えるようになります。

まずは、以下のように<font color="#d14">`format`</font> キーワード引数を両方のViewに追加するとこから始めましょう。

```python:
def snippet_list(request, format=None):
```

```python:
def snippet_detail(request, pk, format=None):
```

それでは、既存のURLに加えて、<font color="#d14">`format_suffix_patterns`</font>のセットを追加し、<font color="#d14">`snippets/urls.py`</font> ファイルを少しだけ更新しましょう。

```python:
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns)
```

これらのURLパターンを必ずしも追加する必要はありませんが、特定のフォーマットを指定するのに、シンプルでわかりやすい方法を提供してくれます。

## ✏️補足
ここでは、URLだけでレスポンスの形式を切り替える方法について説明します。

このチュートリアルでも紹介されているように、`format_suffix_patterns()` 関数を使うことで、拡張子（`.json` や `.api` など）付きのURLにも対応できるようになります。

```python:
urlpatterns = format_suffix_patterns(urlpatterns)
# http://localhost:8000/snippets/1.json
# http://localhost:8000/snippets/1.api
# 上記の '.json'や'.api'がフォーマット接尾辞です
```

このように `.json` や `.api` がURLに含まれている時、`format_suffix_patterns()`関数がその部分を取り出し、View関数の `format` 引数に渡します。

```python:
def snippet_list(request, format=None):
```

DRFが渡された `format` の値に基づいて、Responseオブジェクトを返すときに 適切な Renderer（JSONRenderer, BrowsableAPIRendererなど）を自動的に選択し、レスポンスを出力します。

### 現状の確認

[チュートリアルのパート1](https://qiita.com/drafts/b028848c0bf62a9fd1a0/edit)と同じように、コマンドラインからAPIをテストしてみてください。
ほとんど同じように動作していますが、無効なリクエストを送ったときのエラーハンドリングが少し良くなっています。

前回と同じように Snippet の一覧を取得できます。

```
http http://127.0.0.1:8000/snippets/

HTTP/1.1 200 OK
...
[
  {
    "id": 1,
    "title": "",
    "code": "foo = \"bar\"\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  },
  {
    "id": 2,
    "title": "",
    "code": "print(\"hello, world\")\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
  }
]
```
レスポンスのフォーマットは、Accept ヘッダーを使って制御することができます：
```
http http://127.0.0.1:8000/snippets/ Accept:application/json  # JSON をリクエスト
http http://127.0.0.1:8000/snippets/ Accept:text/html         # HTML をリクエスト
```

もしくは、フォーマット接尾時を追加して制御することできます：
```
http http://127.0.0.1:8000/snippets.json  # 拡張子で JSON を指定
http http://127.0.0.1:8000/snippets.api   # ブラウズ可能な API 表示を指定
```

同じように、<font color="#d14">`Content-Type`</font> ヘッダーを使うことで送信するリクエストのフォーマットを制御することもできます。

```
# POST using form data
http --form POST http://127.0.0.1:8000/snippets/ code="print(123)"

{
  "id": 3,
  "title": "",
  "code": "print(123)",
  "linenos": false,
  "language": "python",
  "style": "friendly"
}

# POST using JSON
http --json POST http://127.0.0.1:8000/snippets/ code="print(456)"

{
    "id": 4,
    "title": "",
    "code": "print(456)",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}
```

上記の <font color="#d14">`http`</font> リクエストに <font color="#d14">`--debug`</font> オプションを追加すると、リクエストヘッダー内のリクエストの種類を確認することができます。

では、Webブラウザで http://127.0.0.1:8000/snippets/ にアクセスして、API を開いてみましょう。

### ブラウザでの閲覧性
APIは、クライアントのリクエストに基づいてレスポンスのコンテンツタイプを選択するため、デフォルトではWebブラウザからリソースがリクエストされた場合、HTML形式で整形されたリソースを返します。これにより、APIはWebブラウザ上で閲覧可能なHTML形式のレスポンスを提供できるようになります。

Webブラウザで閲覧可能なAPIを持つことは、ユーザビリティの面で非常に大きな利点となり、APIの開発や利用が格段に簡単になります。また、他の開発者があなたのAPIを調査・利用しようとする際の参入障壁を大幅に下げる効果もあります。

ブラウズ可能なAPIの機能やカスタマイズ方法について詳しくは、[browsable API](https://www.django-rest-framework.org/topics/browsable-api/) のトピックをご覧ください。

### 次のステップ
[<font color="#d14">チュートリアルのパート3</font>](https://qiita.com/yutty-kov/items/b320e75411f3fc984b82)では、クラスベースビューを使い、ジェネリックビューによってどの程度コード量を削減できるかを見ていきます。