Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django NinjaによるAPI開発の効率化とリプレースの実践

Django NinjaによるAPI開発の効率化とリプレースの実践

Kashun Yoshida

February 22, 2025
Tweet

More Decks by Kashun Yoshida

Other Decks in Programming

Transcript

  1. Who am I? / お前誰よ 吉田花春 (Yoshida Kashun) 𝕏 @kashew_nuts

    BeProud Inc. Software Developer 環境整備と岩登りが趣味 3 / 56
  2. Django Ninja の概要(サンプル) from ninja import NinjaAPI, Schema api =

    NinjaAPI() class User(Schema): id: int username: str @app.get("/users/", response=list[User]) def get_users(request): return [{"id": 1, "username": "kashew"}] # -> /api/users/ 10 / 56
  3. DRFの場合(サンプル) from rest_framework.views import APIView class UserListAPIView(APIView): @extend_schema( # drf_spectacularでOpenAPIドキュメントを記述

    request=UserRequestSerializer, responses=UserResponseSerializer, examples=[ OpenApiExample("ID", value=...), OpenApiExample("name", value=...), ... ], ), def get(self, request): return [{"id": 1, "username": "kashew"}] 13 / 56
  4. FastAPIの場合(サンプル) from fastapi import FastAPI from pydantic import BaseModel, Field

    app = FastAPI() class User(BaseModel): id: int = Field(..., title="ID", examples=[1]) username: str = Field(..., title="ユーザー名", examples=["kashew"]) @app.get("/users/", response_model=list[User]) def get_users(): return [{"id": 1, "username": "kashew"}] # -> /api/users/ 15 / 56
  5. Django Ninja/ FastAPI / Django REST Framework(DRF)比較 Django Ninja FastAPI

    DRF シンプルな設計と 高速なパフォーマ ンス Django の機能を活かし つつ、型ヒントを活用 した直感的な開発 型ヒントを活用し 直感的な開発 Django の機能を 活用しながら API を構築 型ヒントで自動バ リデーションとド キュメント生成 Pydantic を利用した容 易なバリデーション Pydantic を利用し た容易なバリデー ション 独自のシリアラ イザーを利用 Django とのシーム レスな統合 シームレスな統合 Django の統合が 難しい シームレスな統 合 16 / 56
  6. セットアップ & ルーティング $ pip install django-ninja Djangoのurls.pyにルーティング情報を追加 from django.urls

    import path from ninja import NinjaAPI api = NinjaAPI() @api.get("/hello") def hello(request): return {"message": "Hello World"} urlpatterns = [ path("api/", api.urls), ] 22 / 56
  7. Django NinjaがPydanticを使ってバリデーションする例 # schemas.py >> from ninja import Field, Schema

    # SchemaはPydanticのBaseModelを継承 >> class UserSchema(Schema): >> id: int >> name: str = Field(alias="username") # <- aliasを定義 >>> UserSchema(id=1, name="kashew") >>> print(user) UserSchema(id=1, name='kashew') >>> UserSchema(id="not number", name="kashew") # intにstrを指定するとエラーが発生 ... 中略 ... pydantic_core._pydantic_core.ValidationError: 1 validation error for UserSchema id Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not number', input_type=str] 25 / 56
  8. OpenAPIドキュメントの自動生成 /api/docs にアクセス 実装とドキュメントのスキーマ定義を常に最新化 OpenAPIドキュメントをSwagger UI or Redocで自動生成 from ninja

    import Redoc api = NinjaAPI( title="Demo API", description="This is a demo API with dynamic OpenAPI info section" docs=Redoc(), # デフォルトはSwagger() ) 26 / 56
  9. スキーマ定義を充実させてOpenAPIドキュメントと連携 Fieldにtitleやdescription、exampleを記載 リクエストやレスポンスにスキーマを記載してデータ変換&OpenAPIに反映 from ninja import Field, Schema class UserSchema(Schema):

    username: str = Field(..., title="ユーザー名", examples=["kashew"]) email: str = Field(..., title="Email", examples=["[email protected]"]) @api.get("/users/", response=UserSchema) def create_user(request, payload: UserSchema): user = User.objects.create(**payload.dict()) return user 27 / 56
  10. 認証 (サンプル) Djangoの認証を活用した場合 from ninja.security import django_auth api = NinjaAPI(auth=django_auth)

    @api.get("/secure") def secure_endpoint(request): return {"message": f"Hello {request.auth}"} # -> {"message": "Hello kashew"} 32 / 56
  11. 認証(事例紹介) サードパーティと組み合わせた事例を参照。 JSON Web Token(JWT): Simple JWTと組み合わせ OAuth2: django-oauth-toolkit と組み合わせ

    [1] https://github.com/vitalik/django-ninja/issues/45#issuecomment-1049829818 [2] https://github.com/vitalik/django-ninja/issues/1015#issuecomment-1899359588 33 / 56
  12. スロットリング 比較的最近のv1.2.0で同梱された。 IPやユーザー単位でのレート制限を実装 from ninja.throttling import AnonRateThrottle, AuthRateThrottle api =

    NinjaAPI( throttle=[ AnonRateThrottle("10/s"), # IP単位。認証されていないユーザ向け。 AuthRateThrottle("100/s"), # 認証したユーザー単位 ], ) 35 / 56
  13. モジュール分割(Routerを活用) 複数のDjangoアプリケーションで構成されているケース → それぞれのDjangoアプリにAPIを追加したい ├── myproject │ └── settings.py ├──

    events/ │ ├── __init__.py │ └── models.py ├── groups/ │ ├── __init__.py │ └── models.py ├── users/ │ ├── __init__.py │ └── models.py └── manage.py 37 / 56
  14. モジュール分割(Routerを活用) APIを追加するには、各アプリケーションにapi.pyモジュールを作成 ├── myproject │ └── settings.py ├── events/ │

    ├── __init__.py │ ├── api.py │ └── models.py ├── groups/ │ ├── __init__.py │ ├── api.py │ └── models.py ├── users/ │ ├── __init__.py │ ├── api.py │ └── models.py └── manage.py 38 / 56
  15. モジュール分割(Routerを活用) NinjaAPIクラスの代わりに、Routerクラスを使用 from ninja import Router from .models import Event

    router = Router() # <- not NinjaAPI @router.get("/") def list_events(request): return [ {"id": e.id, "title": e.title} for e in Event.objects.all() ] @router.get("/{event_id}") def event_details(request, event_id: int): event = Event.objects.get(id=event_id) return {"title": event.title, "details": event.details} 39 / 56
  16. モジュール分割(Routerを活用) 各モジュールから全Routerをimportし、メインのNinjaAPIインスタンスに取り込む ├── myproject │ ├── api.py │ └── settings.py

    ├── events/ │ ... from ninja import NinjaAPI api = NinjaAPI() # 認証やスロットリングも一括管理できる api.add_router("/events/", "events.api.router") # by Python path # ... 40 / 56
  17. テスト・運用時の注意 Django 標準の TestCase / pytest + pytest-django バージョン管理( api_v1

    , api_v2 )や例外ハンドリング デコレーターで Django の機能を活かす(例: cache_page ) 41 / 56
  18. テスト方法 Ninjaには django.test.TestCase ベースのテストクライアントがある もちろんDjango標準のテストクライアントやpytest + pytest-django も使用可能 個人的には pytest

    + pytest-django がオススメ # pytestの例 import pytest from django.urls import reverse @pytest.mark.django_db def test_create_user(client): target_url = reverse("api-1.0.0:create_user") payload = {"id": 1, "username": "Alice", "email": "[email protected]"} response = client.post(target_url, payload) assert response.status_code == 200 assert "user_id" in response.json() 42 / 56
  19. APIのバージョン管理 1つのDjangoプロジェクトから複数のAPIバージョンを作成可能 NinjaAPIインスタンスを2つ以上、異なるバージョン引数で作成する api_v1 = NinjaAPI(title="API V1", version="1.0.0") api_v2 =

    NinjaAPI(title="API V2", version="2.0.0") # -> http://127.0.0.1/api/v1/docs @api_v1.get("/users") def get_users_v1(request): return [...] # -> http://127.0.0.1/api/v2/docs @api_v2.get("/users") def get_users_v2(request): return [...] 43 / 56
  20. CORS対応 django-cors-headers を併用 フロントエンドとの連携をスムーズに # settings.py INSTALLED_APPS += ["corsheaders",] MIDDLEWARE

    += [ # ... "corsheaders.middleware.CorsMiddleware" "django.middleware.common.CommonMiddleware", # ... ] # 必要に応じてスコープを制限 CORS_ALLOW_ALL_ORIGINS = True 44 / 56
  21. 例外ハンドリング 独自の例外クラスごとにハンドラーを定義可能 @api.exception_handler(ServiceUnavailableError) def service_unavailable(request, exc): return api.create_response( request, {"message":

    "Please retry later"}, status=503, ) @api.get("/service") # some logic that throws exception def some_operation(request): if random.choice([True, False]): raise ServiceUnavailableError() # some logic that throws exception return {"message": "Hello"} 45 / 56
  22. デコレーターの活用 Django標準やサードパーティのデコレーターを活用する from django.views.decorators.cache import cache_page from ninja.decorators import decorate_view

    @api.get("/test") # cache_page(5) # <!-- will not work @decorate_view(cache_page(5)) # It works! def test_view(request): return str(datetime.now()) 46 / 56
  23. Appendix: クラスベースでの対応 Django Ninja Extra: Ninjaのサードパーティを活用することで可能 from ninja_extra import api_controller,

    ControllerBase, permissions, route @api_controller("users/") class UsersController(ControllerBase): @route.get('', response={200: list[UserSchema]}) def get_users(self): users = User.objects.all() return users 47 / 56
  24. 導入後の変化 Before / After API ドキュメント: 手動 HTML → OpenAPI

    自動生成 ビューの可読性: 独自実装 → デコレーター & Schema でスッキリ JSONP 廃止: 実際の利用状況をログで確認 → 不要と判断 49 / 56
  25. 導入後の変化 2:view の記述 Before: ファットコントローラー気味 After: 3 項目ぐらいになり、記述がスッキリ decorator で紐づけ:エンドポイント、

    レスポンススキーマ、サマリーなど リクエスト情報を関数の引数に付与 ビジネスロジックの呼び出し from ninja import Query, Router router = Router(tags=["イベント"]) @router.get("/", response=EventListOut, summary="一覧", ...) def event_search(request, query_params: Query[EventIn]): events = event_search_api(**query_params.dict()) return events 51 / 56
  26. まとめ & 今後の展望 Django Ninja 導入の3大メリット i. Django 資産の最大活用(設定や ORM

    をそのまま使える) ii. 型ヒント & 自動ドキュメントによる保守性向上 iii. パフォーマンス面も良好、学習コストも低め リプレースの結果、開発・運用負荷が低減した 今後認証の仕組みを導入し、使い始めるための手間を減らしていく Django Ninjaのユースケース増加によりコミュニティ拡大を期待 53 / 56