【Django】Django REST FrameworkでシンプルなTodo APIを作ってみる

Django

こんにちは、かつコーチです。

設計・アーキテクチャ編で扱った考え方を、ここで実際に手を動かして確認します。

この記事では、DRF(Django REST Framework)でシンプルなTodo APIを最短ルートで組み立てます。

モデル定義からシリアライザ、ViewSet、ルーティングまで一気通貫で作り、最後に動作確認まで行います。

プロジェクトの準備

DRFのインストールと設定

pip install djangorestframework
# settings.py
INSTALLED_APPS = [
    # ...
    "rest_framework",
    "todos",
]

todosという名前でアプリを作成し、Todo管理機能をこのアプリにまとめます。

python manage.py startapp todos

モデルを定義する

# todos/models.py
from django.db import models

class Todo(models.Model):
    class Priority(models.TextChoices):
        LOW = "low", "低"
        MEDIUM = "medium", "中"
        HIGH = "high", "高"

    title = models.CharField(max_length=200)
    is_done = models.BooleanField(default=False)
    priority = models.CharField(
        max_length=10, choices=Priority.choices, default=Priority.MEDIUM
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

models.TextChoices(Django 3以降の選択肢定義の書き方)を使うと、優先度のような固定の選択肢を型安全に扱えます。

マイグレーションを実行します。

python manage.py makemigrations todos
python manage.py migrate

シリアライザとViewSetを組み立てる

シリアライザの定義

前回解説した通り、シリアライザの責務は「変換」と「入力バリデーション」に絞ります。

# todos/serializers.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ["id", "title", "is_done", "priority", "created_at"]
        read_only_fields = ["created_at"]

    def validate_title(self, value):
        if not value.strip():
            raise serializers.ValidationError("タイトルは空にできません")
        return value

ModelViewSetでCRUDをまとめて実装する

DRFのModelViewSetを使うと、一覧・作成・詳細取得・更新・削除の5つの処理をほぼコードなしで実装できます。

# todos/views.py
from rest_framework import viewsets
from .models import Todo
from .serializers import TodoSerializer

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

ルーティングをDefaultRouterに任せる

# todos/urls.py
from rest_framework.routers import DefaultRouter
from .views import TodoViewSet

router = DefaultRouter()
router.register("todos", TodoViewSet)

urlpatterns = router.urls
# config/urls.py
from django.urls import path, include

urlpatterns = [
    path("api/", include("todos.urls")),
]

DefaultRouter/api/todos/(一覧・作成)と/api/todos/{id}/(詳細・更新・削除)のURLを自動生成します。

動作確認とつまずきポイント

DRFのブラウザブルAPIで確認する

python manage.py runserverを起動し、ブラウザでhttp://127.0.0.1:8000/api/todos/にアクセスすると、DRF標準のブラウザブルAPI画面からPOST・GETの動作確認ができます。

curl -X POST http://127.0.0.1:8000/api/todos/ \
  -H "Content-Type: application/json" \
  -d '{"title": "記事を書く", "priority": "high"}'

❌ Before:認証なしで全ユーザーが操作できてしまう

このままだと、誰でも他人のTodoを削除・変更できてしまいます。

筆者は最初の検証時にこの状態のままAPIを公開設定にしてしまい、認証なしでDELETEリクエストが通ることに気づかず焦った経験があります。

✅ After:IsAuthenticatedとユーザー紐付けを追加する

# todos/models.py
from django.conf import settings

class Todo(models.Model):
    # ...
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# todos/views.py
from rest_framework.permissions import IsAuthenticated

class TodoViewSet(viewsets.ModelViewSet):
    serializer_class = TodoSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Todo.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

get_queryset()をオーバーライドし、ログインユーザー自身のTodoしか見えないように絞り込みます。

perform_create()で保存時に自動的にownerを紐付けることで、リクエストのたびに毎回クライアント側からuser_idを送る必要もなくなります。

応用:フィルタリングとページネーション

一覧APIが育ってくると、完了済みだけ絞り込みたい、優先度で並び替えたいというニーズが出てきます。

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}
class TodoViewSet(viewsets.ModelViewSet):
    # ...
    filterset_fields = ["is_done", "priority"]

django-filter(別途pip install django-filterが必要)を組み込むだけで、/api/todos/?is_done=trueのようなクエリパラメータでの絞り込みが使えるようになります。

まとめ

この記事のポイント

  • ModelViewSetDefaultRouterを使えば、CRUD APIを最小限のコードで構築できる
  • 認証を入れない状態は事故のもと。IsAuthenticatedget_queryset()の絞り込みは必須
  • perform_create()でリクエストユーザーを自動紐付けする
  • django-filterでクエリパラメータによる絞り込みを簡単に追加できる

次に読むべき記事

Django編最終回として、軽量フレームワークのFlaskとの比較検証をお届けします。

→ 次の記事:DjangoとFlask、初心者に向いているのはどちらか比較検証

タグ: #Django #上級者向け #実践プロジェクト

タイトルとURLをコピーしました