こんにちは、iimonでサーバーサイドエンジニアをしています。腰丸です。 本記事はiimonアドベントカレンダー17日目の記事となります。
はじめに
iimonでは、主に「Django + DRF」を使ってAPIサーバーの実装をしています。DRF自体は高機能で、便利なライブラリですが、 ロジックが分散がされて、確認したいコードにたどり着くのが難しかったり、DRFの機能を理解していないと無駄なコードを書いてしまったり、 という難しさを感じています。
DRF自体が悪いわけではないのですが、なにかDRF以外を使用して、良い感じにAPIを実装する方法はないかと考えていたところ 「Django Ninja」の使い心地が良かったので、Django Ninjaを使ったディレクトリ構成について考えてみました。
Django Ninjaとは
Django Ninjaは、Djangoフレームワーク用のAPI開発ツールです。 Pydanticによる厳格な型チェックと自動ドキュメント生成により、Swagger(OpenAPI)の生成もおこなってくれます。(ありがたい)
使用方法については、公式ドキュメントが充実しているので、そちらを参照してもよいですが、下記の記事が、日本語できれいにまとまっていて、とても参考になりました。
Django Ninjaのディレクトリ構成例
さっそくですが、最終的にこんな感じのディレクトリ構成が良いのではないかと思っています。
myproject/ │ ├── app/ # Djangoプロジェクト │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── manage.py │ └── myapp/ │ ├── urls.py │ ├── models/ │ ├── __init__.py │ |── user.py # userモデル │ └── book.py # bookモデル │ └── api/ │ ├── auth/ # 認証関連のAPI機能 │ ├── views.py # auth ビュー層 │ ├── service.py # auth サービス層 │ ├── schema/ │ │ ├── request/ │ │ │ └── ... # リクエストスキーマ │ │ ├── response/ │ │ │ └── ... # レスポンススキーマ │ │ └── dto/ │ │ └── ... # DTOスキーマ │ └── repository/ │ └── user_repository.py # Userリポジトリ │ └── user/ # ユーザー情報関連のAPI(構成はauthと同様) │ └── books/ # 本情報関連のAPI機能(構成はauthと同様) ...
ディレクトリ構成について
view.py
- 入出力のバリデーションや、サービス層の呼び出しのみを行います。
service.py
- ビジネスロジックを記述します。
repository/
- SQLの生成、実行などのデータベースの操作に関連するコードを記述します。
schema/
実装例
ルーティング
- プロジェクトのurls.pyにアプリのルートパスを設定
app/urls.py
from django.contrib import admin from django.urls import path from myapp.urls import myapp_api urlpatterns = [ path("myapp/", myapp_api.urls), path('admin/', admin.site.urls), ]
- myapp/urls.py配下にこのアプリで使用する機能ごとのルートパスを設定 (ファイル名は、ルーティングに関する設定としてurls.pyとしました)
myapp/urls.py
from ninja import NinjaAPI from myapp.api.auth.views import auth_router from myapp.api.user.views import user_router from myapp.api.books.views import books_router myapp_api = NinjaAPI(title="myapp API", version="1.0.0") myapp_api.add_router("auth", auth_router) myapp_api.add_router("user", user_router) myapp_api.add_router("books", books_router)
- views.pyに、その機能に関するルーティングを設定
myapp/api/auth/views.py
from ninja import Router ... auth_router = Router() # POST /myapp/auth/signup というルーティングになります。 @auth_router.post("/signup", response=TokenResponse) def sign_up(request, data: SignUpRequest) -> TokenResponse:
api/配下の実装例(myapp/api/auth/)
Schemaの定義
myapp/api/auth/schemas/request/signup_request.py
from ninja import Schema class SignUpRequest(Schema): username: str password: str email: str
myapp/api/auth/schemas/request/signup_response.py
from ninja import Schema from myapp.api.auth.schema.dto.user_dto import UserDto class SignUpResponse(Schema): user_info: UserDto access_token: str
myapp/api/auth/schemas/request/user_dto.py
from ninja import Schema from myapp.models import User class UserDto(Schema): id: int username: str email: str def __init__(self, user_info: User): super().__init__(id=user_info.id, username=user_info.username, email=user_info.email)
views層の実装
myapp/api/auth/views.py
from ninja import Router from myapp.api.auth.schema.request.login_request import LoginRequest from myapp.api.auth.schema.request.sign_up_request import SignUpRequest from myapp.api.auth.schema.response.sign_up_response import SignUpResponse from myapp.api.auth.service import AuthService auth_router = Router() @auth_router.post("/signup", response=SignUpResponse) def sign_up(request, data: SignUpRequest) -> SignUpResponse: auth_service = AuthService() response = auth_service.sign_up(username=data.username, email=data.email, password=data.password) return response
data: SignUpRequestには、リクエストのデータが入ってきます。 response: SignUpResponseには、レスポンスのデータを定義します。 期待する型定義クラスを書いていることで、このAPIのリクエスト、レスポンスが確認しやすくて良きです。
service層の実装
myapp/api/auth/service.py
import datetime import jwt from django.conf import settings from django.contrib.auth import authenticate from django.db import transaction from myapp.api.auth.repository.user_repository import UserRepository from myapp.api.auth.schema.dto.user_dto import UserDto from myapp.api.auth.schema.response.sign_up_response import SignUpResponse class AuthService: """認証に関するサービス""" def __init__(self): self.user_repository = UserRepository() @transaction.atomic def sign_up(self, username: str, email: str, password: str) -> SignUpResponse: user_dto: UserDto = self.user_repository.create_new_user(username=username, email=email, password=password) token = self._generate_token(user_dto.id) response = SignUpResponse(user_info=user_dto, access_token=token) return response def _generate_token(self, user_id: int) -> str: issued_at = datetime.datetime.utcnow() expired = issued_at + datetime.timedelta(days=1) payload = { 'id': user_id, 'exp': expired, 'iat': issued_at } return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
リポジトリの呼び出しと、トランザクションの設定を行っています。
user_dto: UserDto = self.user_repository.create_new_user(username=username, email=email, password=password)
リポジトリの中で、クエリセットの評価を終わらせて、リポジトリからはDTOをもらうようにしました。戻り値の型を明示的に表現しています。
repository層の実装
myapp/api/auth/repository/user_repository.py
from myapp.api.auth.schema.dto.user_dto import UserDto from myapp.models import User class UserRepository: def create_new_user(self, username: str, email: str, password: str) -> UserDto: user = User.objects.create_user(username=username, email=email, password=password) user_dto = UserDto(user_info=user) return user_dto
SQLの生成と実行から、モデルのインスタンスをDTOに変換して返すまでを行っています。
雑感
もっと勉強しなければ何も語れないのですが、 Django Ninjaの使用感はすごく良かったです。記事では触れてないですが、Swaggerを自動で作ってくれるのも助かります。 実際にはエラーハンドリングや、機能をまたぐ共通処理をどうやっておこなうかなど、他にも考えることがあるので、 良い構成はないか引き続き考えたくなりました。
Djangoを運用するなかでどの部分でどんなクエリを出しているのかが、曖昧になってしまう点を課題に感じていたので、 はっきりとデータベース操作は分ける構成にすることで、SQLの内容を把握しやすくしたいというモチベーションがありました。 リポジトリ層を作成するという構成で考えてみましたが、これで良いのかどうかは検討が必要だと思っています。
最後に
最後まで読んでくださりありがとうございます!
この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします! Wantedly / Green
次回はエンジニアとしての経験が豊富なhoge1さんです!どんな記事か楽しみです!