iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Django Ninjaを使ったDjangoのディレクトリ構成を考えたい

こんにちは、iimonでサーバーサイドエンジニアをしています。腰丸です。 本記事はiimonアドベントカレンダー17日目の記事となります。

はじめに

iimonでは、主に「Django + DRF」を使ってAPIサーバーの実装をしています。DRF自体は高機能で、便利なライブラリですが、 ロジックが分散がされて、確認したいコードにたどり着くのが難しかったり、DRFの機能を理解していないと無駄なコードを書いてしまったり、 という難しさを感じています。

DRF自体が悪いわけではないのですが、なにかDRF以外を使用して、良い感じにAPIを実装する方法はないかと考えていたところ 「Django Ninja」の使い心地が良かったので、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と同様)
        ...

ディレクトリ構成について

  • アプリケーション名/api/配下に、APIの機能ごとにディレクトリを作成しています。api/配下の内容は以下の通りです。

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さんです!どんな記事か楽しみです!