Django DRF #1 DRF のはじめ方 — Serializer、ViewSet、Router
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
作るのは小さな ブログ API — Post、Comment、Author ドメイン。記事の CRUD、コメント、認証、検索 / ページネーション、重い作業は Celery、スキーマ自動ドキュメント、最後にコンテナでデプロイまで。各回はその上に一段ずつ積み重ねていきます。
DRF の位置づけ — フルスタック Django とどう違うか #
基礎 #4 ~ #6 で見たフルスタック Django は テンプレート + フォーム + サーバーレンダリング の流れでした。ユーザーがブラウザでフォームを送信すると、view が HttpResponse(render(...)) で HTML を返す形でしたね。
最近の SPA (React/Vue/Next) + モバイルアプリの組み合わせでは、サーバーは JSON だけ 返し、画面はクライアントが描きます。ここに入るのが DRF です。
| フルスタック Django | Django + DRF | |
|---|---|---|
| レスポンス | HTML (template) | JSON |
| 入力 | Form / ModelForm | Serializer |
| 認証 | session + cookie | Token / JWT (+ session) |
| クライアント | ブラウザ (サーバーレンダリング) | SPA / モバイル / 外部 |
| 自動ドキュメント | 別途 | drf-spectacular (#5) |
| 学習曲線 | なだらか | なだらか + DRF の追加概念 |
核心は Django ORM、auth、admin、signals のような基盤はそのまま持ってくる という点です。DRF はその上に入力 / 出力を JSON に変える薄いレイヤーです。
FastAPI との比較 #
同じ領域 (JSON API) に FastAPI もあります。比較表はこの程度。
| Django + DRF | FastAPI | |
|---|---|---|
| スタイル | フルスタックの上に API レイヤー | マイクロ + 型 |
| ORM | Django ORM (成熟) | 外部 (SQLAlchemy など) |
| 認証 / 権限 | ビルトイン (auth、permissions) | 自前で組み立て |
| Admin | ビルトインで強力 | なし (外部) |
| マイグレーション | makemigrations/migrate | Alembic (外部) |
| 自動ドキュメント | drf-spectacular (別途) | ビルトイン |
| 非同期 | 部分的 (4.0+) | ネイティブ |
| データ検証 | Serializer | Pydantic |
| 向く場面 | 運用中心、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 blogINSTALLED_APPS の登録
#
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 つで追加されます。追加のミドルウェア変更なしですぐに動作します。
ドメインモデル #
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 createsuperuserSerializer — DRF の入力 / 出力スキーマ #
Serializer は Python オブジェクト ↔ JSON 変換 + 検証 を担当します。Django の Form/ModelForm と形が似ていますが、HTML ではなく JSON を扱うのが核心の違い。
最も基本 — serializers.Serializer
#
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 のフィールドは 出力にだけ 入ります (サーバーが作る id、created_at)。
ModelSerializer — モデルから自動推論
#
ほとんどの場合はモデルをそのままシリアライズします。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 フィールドを推論 (
CharField→serializers.CharField) create()、update()のデフォルト実装を提供- モデルの
unique=True、max_lengthなどの検証を自動で反映
fields = "__all__" で全フィールドを公開することもできますが、明示的に書くのが安全 です — 新しいフィールドがモデルに追加されると意図せず公開される可能性があります。
read_only_fields の意味
#
author を read-only にした理由: クライアントが他人の記事として偽造できないように。ViewSet の perform_create で request.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 — 最も原始的 #
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 — 共通パターンの抽出 #
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 = PostSerializerDRF がよく使う組み合わせを事前に用意したクラス群。
| クラス | メソッド |
|---|---|
ListAPIView | GET (list) |
CreateAPIView | POST |
ListCreateAPIView | GET + POST |
RetrieveAPIView | GET (detail) |
UpdateAPIView | PUT/PATCH |
DestroyAPIView | DELETE |
RetrieveUpdateDestroyAPIView | GET + PUT/PATCH + DELETE |
一覧と詳細を 2 つのクラスに分けて使う形。URL も 2 つに。
ViewSet — 1 クラスで CRUD 全体 #
ViewSet は 1 つのクラスに list/retrieve/create/update/destroy をまとめ、Router が URL を自動で作ります。DRF の標準パターン。
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 | 動作 |
|---|---|---|---|
list | GET | /posts/ | 一覧 |
retrieve | GET | /posts/{id}/ | 詳細 |
create | POST | /posts/ | 作成 |
update | PUT | /posts/{id}/ | 全体更新 |
partial_update | PATCH | /posts/{id}/ | 部分更新 |
destroy | DELETE | /posts/{id}/ | 削除 |
perform_create() は save 時点で追加データ を差し込むときに使うフック。上では author を現在のリクエストユーザーに差し込んでいます。
その他のよく使うフック #
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 PostSerializerget_queryset()— アクション / ユーザー / 権限ごとに動的な querysetget_serializer_class()— list は軽いシリアライズ、retrieve/create は詳しいシリアライズのような分離
@action — 追加エンドポイント
#
標準 CRUD 以外にアクションを増やしたいとき。
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 がそれを自動でやります。
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.urlsfrom django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("blog.urls")),
]これで終わりです。DefaultRouter が作ってくれる 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。
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
# "rest_framework.renderers.BrowsableAPIRenderer", # 開発のみ
],
}最初の成果物 — 一画面に #
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"]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)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.urlsuv 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_APPSにrest_framework一行追加- Serializer — Python ↔ JSON + 検証
serializers.Serializer(手動)、ModelSerializer(自動)read_only_fieldsでサーバー決定フィールドを保護
- 検証の 3 つの箇所 —
validate_<field>、validate、関数型 validator - View 階層 —
APIView→GenericAPIView+ mixins → 短縮形 →ViewSet ModelViewSet— 1 クラスで CRUD、perform_createで author を差し込み@actionで追加エンドポイント (publish、drafts)- Router —
DefaultRouter().register()で URL 自動生成 - Browsable API — ブラウザですぐテスト可能な HTML インターフェース
回帰リンク: 基礎 #3 Models と ORM のモデル定義がそのまま入ってきており、基礎 #7 Admin と認証 の User モデルが author ForeignKey で繋がっています。
次回 (#2 認証 / 権限) では 誰がその記事を書いて修正できるか — Token/JWT 認証、IsAuthenticated/IsOwner のような permission クラスを扱います。