Django DRF #1 DRF のはじめ方 — Serializer、ViewSet、Router

読了 9分

Django 基礎中級上級 21 編の道具が一カ所に集まるシリーズです。フルスタック Django がテンプレート / フォーム / セッションをそのまま使うのに対し、DRF (Django REST Framework) は同じ ORM・認証の基盤の上に REST API レイヤー をきれいに乗せてくれます。

  • #1 DRF のはじめ方 — Serializer、ViewSet、Router ← 今回
  • #2 認証 / 権限 — Token、JWT、custom permission
  • #3 Filtering / Ordering / Pagination
  • #4 Celery で非同期作業
  • #5 OpenAPI ドキュメント (drf-spectacular)
  • #6 テストとデプロイ — Docker、gunicorn、nginx

作るのは小さな ブログ APIPostCommentAuthor ドメイン。記事の CRUD、コメント、認証、検索 / ページネーション、重い作業は Celery、スキーマ自動ドキュメント、最後にコンテナでデプロイまで。各回はその上に一段ずつ積み重ねていきます。

DRF の位置づけ — フルスタック Django とどう違うか #

基礎 #4 ~ #6 で見たフルスタック Django は テンプレート + フォーム + サーバーレンダリング の流れでした。ユーザーがブラウザでフォームを送信すると、view が HttpResponse(render(...)) で HTML を返す形でしたね。

最近の SPA (React/Vue/Next) + モバイルアプリの組み合わせでは、サーバーは JSON だけ 返し、画面はクライアントが描きます。ここに入るのが DRF です。

フルスタック DjangoDjango + DRF
レスポンスHTML (template)JSON
入力Form / ModelFormSerializer
認証session + cookieToken / JWT (+ session)
クライアントブラウザ (サーバーレンダリング)SPA / モバイル / 外部
自動ドキュメント別途drf-spectacular (#5)
学習曲線なだらかなだらか + DRF の追加概念

核心は Django ORM、auth、admin、signals のような基盤はそのまま持ってくる という点です。DRF はその上に入力 / 出力を JSON に変える薄いレイヤーです。

FastAPI との比較 #

同じ領域 (JSON API) に FastAPI もあります。比較表はこの程度。

Django + DRFFastAPI
スタイルフルスタックの上に API レイヤーマイクロ + 型
ORMDjango ORM (成熟)外部 (SQLAlchemy など)
認証 / 権限ビルトイン (auth、permissions)自前で組み立て
Adminビルトインで強力なし (外部)
マイグレーションmakemigrations/migrateAlembic (外部)
自動ドキュメントdrf-spectacular (別途)ビルトイン
非同期部分的 (4.0+)ネイティブ
データ検証SerializerPydantic
向く場面運用中心、admin / ユーザー / 権限が多いサービスマイクロサービス、データ / ML API

すでに Django で作ったモノリシックなサービスなら DRF が自然で、最初から小さな非同期 API だけなら FastAPI が軽いです。このシリーズは DRF 側を深く見ます。

プロジェクトのセットアップ #

基礎 #2 のセットアップの流れの上に DRF を追加します。

プロジェクトを作る
uv init blog-api --python 3.13
cd blog-api
uv add django djangorestframework
uv run django-admin startproject mysite .
uv run python manage.py startapp blog

INSTALLED_APPS の登録 #

mysite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",          # DRF 本体
    "blog",                    # 自分のアプリ
]

DRF は単純にアプリ 1 つで追加されます。追加のミドルウェア変更なしですぐに動作します。

ドメインモデル #

blog/models.py
from django.conf import settings
from django.db import models


class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="posts",
    )
    title = models.CharField(max_length=200)
    body = models.TextField()
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]


class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="comments",
    )
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["created_at"]

基礎 #3 で見た Model 定義そのままです。DRF は Django モデルをそのまま使う のが核心。

マイグレーション
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py createsuperuser

Serializer — DRF の入力 / 出力スキーマ #

Serializer は Python オブジェクト ↔ JSON 変換 + 検証 を担当します。Django の Form/ModelForm と形が似ていますが、HTML ではなく JSON を扱うのが核心の違い。

最も基本 — serializers.Serializer #

blog/serializers.py — 基本形
from rest_framework import serializers


class PostSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    body = serializers.CharField()
    published = serializers.BooleanField(default=False)
    created_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        return Post.objects.create(**validated_data)

    def update(self, instance, validated_data):
        for k, v in validated_data.items():
            setattr(instance, k, v)
        instance.save()
        return instance

フィールドを 1 つずつ直接書く形です。read_only=True のフィールドは 出力にだけ 入ります (サーバーが作る idcreated_at)。

ModelSerializer — モデルから自動推論 #

ほとんどの場合はモデルをそのままシリアライズします。ModelSerializer 一行で終わり。

blog/serializers.py — ModelSerializer
from rest_framework import serializers
from .models import Post, Comment


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "author", "title", "body", "published", "created_at", "updated_at"]
        read_only_fields = ["id", "author", "created_at", "updated_at"]


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ["id", "post", "author", "body", "created_at"]
        read_only_fields = ["id", "author", "created_at"]

ModelSerializer が自動でやってくれること:

  • モデルフィールドから serializer フィールドを推論 (CharFieldserializers.CharField)
  • create()update() のデフォルト実装を提供
  • モデルの unique=Truemax_length などの検証を自動で反映

fields = "__all__" で全フィールドを公開することもできますが、明示的に書くのが安全 です — 新しいフィールドがモデルに追加されると意図せず公開される可能性があります。

read_only_fields の意味 #

author を read-only にした理由: クライアントが他人の記事として偽造できないように。ViewSet の perform_createrequest.user を author に差し込むパターンを #2 で見ます。

検証 — validate_<field>validate、custom validator #

Serializer に検証を載せる箇所は 3 つです。

1) フィールド単位 — validate_<field> #

フィールド単位の検証
class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "body", "published"]

    def validate_title(self, value: str) -> str:
        if value.strip().lower() in {"untitled", "test"}:
            raise serializers.ValidationError("意味のないタイトルは禁止")
        return value.strip()

validate_<フィールド名> メソッドは そのフィールドの値だけ 受け取って検証します。

2) モデル単位 — validate #

複数フィールドを同時に
def validate(self, attrs: dict) -> dict:
    if attrs.get("published") and len(attrs.get("body", "")) < 100:
        raise serializers.ValidationError(
            {"body": "公開記事は最低 100 文字以上である必要があります。"}
        )
    return attrs

複数のフィールドを一緒にチェックするとき。attrs がすべての検証済み値の dict。

3) 再利用可能な validator #

ブログ全体で再利用
def no_html_tags(value: str) -> None:
    if "<script" in value.lower():
        raise serializers.ValidationError("script タグは許可されません")

class PostSerializer(serializers.ModelSerializer):
    body = serializers.CharField(validators=[no_html_tags])
    ...

関数型の validator は複数の serializer で再利用しやすいです。

検証失敗時には自動で 400 Bad Request + どこが問題かを JSON で応答:

検証失敗レスポンスの例
{
  "title": ["意味のないタイトルは禁止"],
  "body": ["公開記事は最低 100 文字以上である必要があります。"]
}

View の階層 — APIView → Generic → ViewSet #

DRF の view は 階層 です。下に行くほど短く、上に行くほど自由です。

階層
APIView         — 最も原始的、get/post/put を直接実装
GenericAPIView  — queryset / serializer_class のような共通属性
  ↓ (+ mixins)
ListAPIView, RetrieveAPIView, CreateAPIView, ...
  ↓ (+ Router)
ViewSet → ModelViewSet  — CRUD 一行

1) APIView — 最も原始的 #

blog/views.py — APIView
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class PostListAPIView(APIView):
    def get(self, request):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = PostSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save(author=request.user)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

Django の CBV と似た形 — get()post() メソッドを直接実装。最も明示的ですが同じコードが繰り返されます。

2) GenericAPIView + mixins — 共通パターンの抽出 #

generic + mixins
from rest_framework import generics, mixins


class PostListCreateAPIView(
    mixins.ListModelMixin,
    mixins.CreateModelMixin,
    generics.GenericAPIView,
):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

queryset + serializer_class の 2 行が核心。mixins が list()create() のようなメソッドを提供。

3) Generic の短縮形 — ListCreateAPIView など #

さらに短く
from rest_framework import generics


class PostListCreateAPIView(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer


class PostDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

DRF がよく使う組み合わせを事前に用意したクラス群。

クラスメソッド
ListAPIViewGET (list)
CreateAPIViewPOST
ListCreateAPIViewGET + POST
RetrieveAPIViewGET (detail)
UpdateAPIViewPUT/PATCH
DestroyAPIViewDELETE
RetrieveUpdateDestroyAPIViewGET + PUT/PATCH + DELETE

一覧と詳細を 2 つのクラスに分けて使う形。URL も 2 つに。

ViewSet — 1 クラスで CRUD 全体 #

ViewSet1 つのクラスに list/retrieve/create/update/destroy をまとめ、Router が URL を自動で作ります。DRF の標準パターン。

blog/views.py — ModelViewSet
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

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

この 4 行が次のすべてを作ります:

メソッドメソッド (HTTP)URL動作
listGET/posts/一覧
retrieveGET/posts/{id}/詳細
createPOST/posts/作成
updatePUT/posts/{id}/全体更新
partial_updatePATCH/posts/{id}/部分更新
destroyDELETE/posts/{id}/削除

perform_create()save 時点で追加データ を差し込むときに使うフック。上では author を現在のリクエストユーザーに差し込んでいます。

その他のよく使うフック #

ViewSet のフック群
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get_queryset(self):
        # 動的に queryset を決定 — 例: 自分の記事だけ
        qs = super().get_queryset()
        if self.action == "list" and self.request.user.is_authenticated:
            return qs.filter(author=self.request.user)
        return qs.filter(published=True)

    def get_serializer_class(self):
        # アクションごとに異なる serializer
        if self.action == "list":
            return PostListSerializer
        return PostSerializer
  • get_queryset() — アクション / ユーザー / 権限ごとに動的な queryset
  • get_serializer_class() — list は軽いシリアライズ、retrieve/create は詳しいシリアライズのような分離

@action — 追加エンドポイント #

標準 CRUD 以外にアクションを増やしたいとき。

@action デコレータ
from rest_framework.decorators import action
from rest_framework.response import Response


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    @action(detail=True, methods=["post"])
    def publish(self, request, pk=None):
        """POST /posts/{id}/publish/ — 記事の公開処理。"""
        post = self.get_object()
        post.published = True
        post.save()
        return Response({"published": True})

    @action(detail=False, methods=["get"])
    def drafts(self, request):
        """GET /posts/drafts/ — 自分の下書き一覧。"""
        qs = self.get_queryset().filter(author=request.user, published=False)
        serializer = self.get_serializer(qs, many=True)
        return Response(serializer.data)
  • detail=True — インスタンス単位 (/posts/{id}/publish/)
  • detail=False — コレクション単位 (/posts/drafts/)

Router — URL の自動生成 #

ViewSet はそれ自体では URL に登録できません — どのメソッドをどの HTTP 動詞にマッピングするか教える必要があります。Router がそれを自動でやります。

blog/urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CommentViewSet

router = DefaultRouter()
router.register(r"posts", PostViewSet, basename="post")
router.register(r"comments", CommentViewSet, basename="comment")

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

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("blog.urls")),
]

これで終わりです。DefaultRouter が作ってくれる URL:

自動生成された URL
GET    /api/posts/                  → list
POST   /api/posts/                  → create
GET    /api/posts/{pk}/             → retrieve
PUT    /api/posts/{pk}/             → update
PATCH  /api/posts/{pk}/             → partial_update
DELETE /api/posts/{pk}/             → destroy
POST   /api/posts/{pk}/publish/     → @action(detail=True) publish
GET    /api/posts/drafts/           → @action(detail=False) drafts
GET    /api/                        → API root (DefaultRouter のみ)

DefaultRouter/api/ ルートに API インデックス も自動で作ってくれます。SimpleRouter はインデックスなしでルートだけ。

Browsable API — DRF の隠れた武器 #

ブラウザで /api/posts/ をそのまま開いてみてください。DRF が HTML インターフェース を自動で作ってくれます — フォームで POST を送り、レスポンスを JSON で見て、認証状態をトグルする UI まで。

API 開発の初期に非常に便利です。プロダクションでオフにしたければ DEFAULT_RENDERER_CLASSES から BrowsableAPIRenderer を外せば OK。

settings.py — プロダクションは JSON のみ
REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": [
        "rest_framework.renderers.JSONRenderer",
        # "rest_framework.renderers.BrowsableAPIRenderer",   # 開発のみ
    ],
}

最初の成果物 — 一画面に #

blog/serializers.py
from rest_framework import serializers
from .models import Post, Comment


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "author", "title", "body", "published", "created_at", "updated_at"]
        read_only_fields = ["id", "author", "created_at", "updated_at"]


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ["id", "post", "author", "body", "created_at"]
        read_only_fields = ["id", "author", "created_at"]
blog/views.py
from rest_framework import viewsets
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

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


class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
blog/urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CommentViewSet

router = DefaultRouter()
router.register(r"posts", PostViewSet)
router.register(r"comments", CommentViewSet)

urlpatterns = router.urls
実行
uv run python manage.py runserver
# → http://127.0.0.1:8000/api/

ブラウザで /api/posts/ を開くと Browsable API が出てきて、フォームから記事を作れます。次回以降の記事がその上に認証 / フィルタリング / 非同期 / ドキュメント / デプロイを一段ずつ積み重ねていきます。

まとめ #

今回つかんだもの:

  • DRF の位置づけ — Django 上の JSON API レイヤー (ORM/auth/admin そのまま)
  • Django + DRF vs FastAPI の比較 — 向く領域が違う
  • INSTALLED_APPSrest_framework 一行追加
  • Serializer — Python ↔ JSON + 検証
    • serializers.Serializer (手動)、ModelSerializer (自動)
    • read_only_fields でサーバー決定フィールドを保護
  • 検証の 3 つの箇所 — validate_<field>validate、関数型 validator
  • View 階層APIViewGenericAPIView + mixins → 短縮形 → ViewSet
  • ModelViewSet — 1 クラスで CRUD、perform_create で author を差し込み
  • @action で追加エンドポイント (publishdrafts)
  • RouterDefaultRouter().register() で URL 自動生成
  • Browsable API — ブラウザですぐテスト可能な HTML インターフェース

回帰リンク: 基礎 #3 Models と ORM のモデル定義がそのまま入ってきており、基礎 #7 Admin と認証User モデルが author ForeignKey で繋がっています。

次回 (#2 認証 / 権限) では 誰がその記事を書いて修正できるか — Token/JWT 認証、IsAuthenticated/IsOwner のような permission クラスを扱います。

X