はじめに
Django REST Framework(以下DRF)は、Django上でREST APIを構築するためのライブラリです。
DjangoのORM・認証機能・Adminと組み合わせることで、短時間で堅牢なAPIを作ることができます。
DRFの基本構成である、Model/Serializer/View/Routerを一通り実装するサンプルとしてシンプルな読書管理APIを実装します。
ER図
このサンプルは以下の3テーブルを作成します。
-
users: ログインユーザー -
books: 書籍マスタ -
user_books: ユーザーごとの読書ステータス
user_booksはユーザーと書籍の中間テーブルとして、読書ステータスやメモなどを管理できるように設計しています。
API完成イメージ
- 認証API
POST /api/v1/auth/register/POST /api/v1/auth/login/POST /api/v1/auth/logout/
- 書籍API(認証不要でAPIからは参照のみ行う)
GET /api/v1/books/GET /api/v1/books/{id}/
- ユーザーごとの読書状態管理用API(ログインユーザーのみ操作)
GET/POST /api/v1/user-books/GET/PUT/PATCH/DELETE /api/v1/user-books/{id}/
作成したコード
プロジェクト環境作成
一番最初に以下のようなディレクトリ構成になるようにします
qiita-drf(プロジェクト名は適宜変更お願いします。)
├── requirements.txt
└── venv
作成コマンド例。
mkdir qiita-drf
cd qiita-drf
python -m venv venv
# 仮想環境の有効化(Linux/Mac)
source ./venv/bin/activate
# Windowsの方は以下を実行
# .\venv\Scripts\activate
今回は仮想環境としてvenvを使用しています。
requirements.txtの内容は以下となっています。
django==5.2.12
djangorestframework==3.16.1
markdown==3.10.2
django-cors-headers==4.9.0
使用するライブラリインストール
requirements.txtのインストールを行います。
python -m pip install --upgrade pip
pip install -r requirements.txt
Djangoプロジェクト作成
プロジェクト作成
以下コマンドでdjangoプロジェクトの作成を行います。
sample_configは適宜自分のプロジェクト名に変えてください。
django-admin startproject sample_config .
アプリ作成
以下コマンドでdjangoアプリの作成を行います。
sample_appは適宜自分のアプリ名に変えてください。
./manage.py startapp sample_app
プロジェクト作成後のディレクトリ構成は以下のようになります
qiita-drf
├── manage.py
├── requirements.txt
├── venv
│ └── ...省略
├── sample_app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── sample_config
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
settings.py設定
+ if DEBUG: # 今回のサンプルコードでは動作確認のためデバック時のみCORSを全許可してます
+ CORS_ALLOW_ALL_ORIGINS = True
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
...
+ 'rest_framework',
+ 'corsheaders',
+ # 自作APPS
+ 'sample_app'
]
+ REST_FRAMEWORK = {
+ "DEFAULT_AUTHENTICATION_CLASSES": [
+ "rest_framework.authentication.SessionAuthentication",
+ ],
+ "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
+ "PAGE_SIZE": 10
+ }
MIDDLEWARE = [
+ 'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
日本語設定 / タイムゾーン設定
- LANGUAGE_CODE = 'en-us'
+ LANGUAGE_CODE = 'ja'
- TIME_ZONE = 'UTC'
+ TIME_ZONE = 'Asia/Tokyo'
modelの定義
今回のサンプルではmodels/にmodelの定義を集約しています。
from .user import UserModel
from .book import BookModel
from .user_book import UserBookModel
__all__ = ["UserModel", "BookModel", "UserBookModel"]
Bookモデル
書籍情報を管理します。
from django.db import models
class BookModel(models.Model):
"""書籍を表すモデル。"""
title = models.CharField("タイトル", max_length=255)
author = models.CharField("著者", max_length=255)
created_at = models.DateTimeField("作成日時", auto_now_add=True)
updated_at = models.DateTimeField("更新日時", auto_now=True)
class Meta:
db_table = "books"
verbose_name = "書籍"
verbose_name_plural = "書籍一覧"
ordering = ["-created_at"]
def __str__(self) -> str:
"""管理画面向けの表示名"""
return f"{self.title} / {self.author}"
UserBookモデル
ユーザーごとの読書状態を管理します。
from django.conf import settings
from django.db import models
class ReadingStatus(models.TextChoices):
"""読書ステータス定義"""
UNREAD = "unread", "未読"
READING = "reading", "読書中"
FINISHED = "finished", "読了"
class UserBookModel(models.Model):
"""ユーザーと書籍の中間モデル"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="ユーザー",
on_delete=models.CASCADE,
related_name="user_books",
)
book = models.ForeignKey(
"sample_app.BookModel",
verbose_name="書籍",
on_delete=models.CASCADE,
related_name="user_books",
)
status = models.CharField("読書ステータス", max_length=20, choices=ReadingStatus.choices, default=ReadingStatus.UNREAD)
memo = models.TextField("メモ", blank=True, default="")
created_at = models.DateTimeField("作成日時", auto_now_add=True)
updated_at = models.DateTimeField("更新日時", auto_now=True)
class Meta:
db_table = "user_books"
verbose_name = "ユーザー書籍"
verbose_name_plural = "ユーザー書籍一覧"
constraints = [
models.UniqueConstraint(fields=["user", "book"], name="unique_user_book"),
]
def __str__(self) -> str:
"""管理画面向けの表示名"""
return f"{self.user} - {self.book} ({self.status})"
ユーザーモデル
今回はemail/password認証を採用しています。
from __future__ import annotations
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.db import models
class UserManager(BaseUserManager["UserModel"]):
"""メールアドレス認証用のユーザーマネージャ。"""
use_in_migrations = True
def create_user(self, email: str, password: str | None = None, **extra_fields: object) -> "UserModel":
"""通常ユーザーを作成する
Args:
email: ログインに利用するメールアドレス
password: 平文パスワード
**extra_fields: 追加のユーザー属性
Returns:
作成されたユーザー
"""
if not email:
raise ValueError("メールアドレスは必須です。")
normalized_email = self.normalize_email(email)
user = self.model(email=normalized_email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email: str, password: str, **extra_fields: object) -> "UserModel":
"""スーパーユーザーを作成する
Args:
email: ログインに利用するメールアドレス
password: 平文パスワード
**extra_fields: 追加のユーザー属性
Returns:
作成されたユーザー
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
return self.create_user(email=email, password=password, **extra_fields)
class UserModel(AbstractUser):
"""email/passwordで認証を行うカスタムユーザーモデル"""
# 今回のサンプルプロジェクトはusername, first_name, last_nameは不要なカラムのためNoneにする
username = None
first_name = None
last_name = None
email = models.EmailField("メールアドレス", unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS: list[str] = []
objects = UserManager()
class Meta:
db_table = "users"
verbose_name = "ユーザー"
verbose_name_plural = "ユーザー一覧"
def __str__(self) -> str:
return self.email
settings.pyにUser設定
+ AUTH_USER_MODEL = "sample_app.UserModel"
modelをdbに反映
./manage.py makemigrations
./manage.py migrate
管理画面ログイン用ユーザー作成
以下コマンドで開発用の管理画面ログイン用ユーザー作成を行います。
メールアドレス・パスワードは適宜入力お願いします。
./manage.py createsuperuser
サーバー立ち上げ
runserverをすると管理画面が表示されるため、先ほど作成した管理画面ログイン用ユーザーのログイン情報でログインを行います。
./manage.py runserver
ログイン後の管理画面
まだadmin.pyの定義をしていないため、作成したテーブルが表示されていません。
次にadmin.pyに管理画面へ表示するmodelの設定を定義します。
admin.pyの定義
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import BookModel, UserBookModel, UserModel
@admin.register(UserModel)
class UserModelAdmin(UserAdmin):
ordering = ("id",)
list_display = ("id", "email", "is_staff", "is_active", "last_login", "date_joined")
search_fields = ("email",)
list_filter = ("is_staff", "is_superuser", "is_active")
fieldsets = (
(None, {"fields": ("email", "password")}),
("権限", {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
("最終ログイン・登録日時", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2", "is_staff", "is_superuser", "is_active"),
},
),
)
@admin.register(BookModel)
class BookModelAdmin(admin.ModelAdmin):
list_display = ("id", "title", "author", "created_at", "updated_at")
search_fields = ("title", "author")
ordering = ("-created_at",)
list_per_page = 30
@admin.register(UserBookModel)
class UserBookModelAdmin(admin.ModelAdmin):
list_display = ("id", "user", "book", "status", "updated_at")
search_fields = ("user__email", "book__title", "book__author", "memo")
list_filter = ("status", "created_at", "updated_at")
autocomplete_fields = ("user", "book")
ordering = ("-updated_at",)
list_select_related = ("user", "book")
list_per_page = 50
admin.py定義後に管理画面を確認すると作成したモデルが表示されていることが確認できます。
API実装
では今回のメインであるDRFのSerializer/View/Routerの実装に入ります。
Serializezr実装
DRFではSerializerを使用してデータの変換とバリデーションを行います。
今回はModelSerializerでサンプル実装してみました。
認証用Serializer
from django.contrib.auth import get_user_model
from rest_framework import serializers
UserModel = get_user_model()
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
class Meta:
model = UserModel
fields = ["id", "email", "password"]
read_only_fields = ["id"]
def create(self, validated_data):
password = validated_data.pop("password")
return UserModel.objects.create_user(password=password, **validated_data)
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
書籍情報用Serializer
from rest_framework import serializers
from sample_app.models import BookModel
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = BookModel
fields = ["id", "title", "author", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
ユーザーごとの読書状態管理用Serializer
from rest_framework import serializers
from sample_app.models import UserBookModel
class UserBookSerializer(serializers.ModelSerializer):
class Meta:
model = UserBookModel
fields = [
"id",
"user",
"book",
"status",
"memo",
"created_at",
"updated_at",
]
read_only_fields = ["id", "user", "created_at", "updated_at"]
views実装
認証用view
from django.contrib.auth import authenticate, get_user_model, login, logout
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from sample_app.serializers.auth import LoginSerializer, RegisterSerializer
UserModel = get_user_model()
class RegisterAPIView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
response_data = {
"id": user.id,
"email": user.email,
}
return Response(response_data, status=status.HTTP_201_CREATED)
class LoginAPIView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
email = serializer.validated_data["email"]
password = serializer.validated_data["password"]
user = authenticate(request, email=email, password=password)
if user is None:
return Response({"detail": "メールアドレスまたはパスワードが正しくありません。"}, status=status.HTTP_401_UNAUTHORIZED)
login(request, user)
return Response({"detail": "ログインしました。"}, status=status.HTTP_200_OK)
class LogoutAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
logout(request)
return Response({"detail": "ログアウトしました。"}, status=status.HTTP_200_OK)
書籍情報用View
from rest_framework import viewsets
from sample_app.models import BookModel
from sample_app.serializers.book import BookSerializer
class BookViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BookModel.objects.order_by("id")
serializer_class = BookSerializer
ユーザーごとの読書状態管理用View
from rest_framework import permissions, viewsets
from sample_app.models import UserBookModel
from sample_app.serializers.user_book import UserBookSerializer
class UserBookViewSet(viewsets.ModelViewSet):
serializer_class = UserBookSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
# ログイン中であるユーザー自身のデータだけ返す
return UserBookModel.objects.filter(user=self.request.user).select_related("book", "user").order_by("-updated_at")
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Router実装
from django.urls import path
from rest_framework.routers import DefaultRouter
from sample_app.views.auth import LoginAPIView, LogoutAPIView, RegisterAPIView
from sample_app.views.book import BookViewSet
from sample_app.views.user_book import UserBookViewSet
router = DefaultRouter()
router.register("books", BookViewSet, basename="books")
router.register("user-books", UserBookViewSet, basename="user-books")
urlpatterns = [
path("auth/register/", RegisterAPIView.as_view(), name="auth-register"),
path("auth/login/", LoginAPIView.as_view(), name="auth-login"),
path("auth/logout/", LogoutAPIView.as_view(), name="auth-logout"),
]
urlpatterns += router.urls
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path("api/v1/", include("sample_app.urls")),
]
if settings.DEBUG:
urlpatterns += [
path("api/v1/", include("rest_framework.urls")),
]
URL追加後、http://127.0.0.1:8000/api/v1/にアクセスすることで、実装したAPIをDRF機能で実行可能です。
まとめ
今回はDjango REST Frameworkを使った簡単な読書管理APIを作成しました。
今回のサンプルをベースにJWT認証やlogging設定などを追加していくことで、より実用的なAPIに発展させることができると思います。
ご拝読いただきありがとうございました!






