Django REST FrameworkなWebアプリをdjango-modern-restにマイグレーション

2年程前にスタートした Django REST Framework (DRF)なプロジェクトを エンドポイントの約99%を django-modern-rest (DMR)を使って書き直した。 例のごとくほぼ100%AIの力を借りて。

この書き換えに関わらず最近はこんな感じで開発している。

  1. 適当にIssueを書く。
  2. Claudeで実装する。(Opus or Fableのxhigh)
  3. Pull-Requestを作ってもらう
  4. Code Rabbit or Codex(Sol Ultra)でレビュー
  5. 適当にClaudeにフィードバックして修正してもらう。必要に応じて何回か繰り返し
  6. 人間がレビュー
  7. マージする

これをひたすら繰り返してる感じ。 DMRへのマイグレーションもこれと同じ。 DRF(v1)のエンドポイントを残しつつ、DMR(v2)のエンドポイントを移植しつつ追加して、 フロントエンド側でv2にリクエストするようにして切り替え。 Django Apps単位で切り替えて行った感じ。 最初の方向付けが大事だと思ったのでADRは書いた。これもAIが書いてAIがレビューして人間もレビューする形。

django-modern-restの良いところ

公式サイトに記載されていることの繰り返しになるかもだけど書き出してみる。

切り替えた時の DMRのバージョンは 0.17.0 です。

シリアライズがmsgspecなので高速

まぁ業務系アプリなのでパフォーマンスの大半はDB部分にあるので、誤差といえば誤差だが速いに越したことはない。 私自身がパフォーマンス原理主義者というのはある。 もう一点良い点としてシリアライザにクエリ(ビジネスロジック)が入り込みにくい。 DRFでもできるのだろうけど、 msgspec はフィールド定義とバリデーションだけしかできないので、入り込む余地がない。

Decimalの細かいバリデーションはできなかったので、 __post__init__ メソッドで自前で行う必要がある。 以下は感じを掴むためのコード断片。

class ItemUpdateRequest(msgspec.Struct, kw_only=True):

    name: Annotated[str, Meta(min_length=1, max_length=500)]
    unit_price: Decimal
    id: int | None | UnsetType = UNSET

    def __post_init__(self) -> None:
        # モデルの DecimalField と同じ桁数(単価 12 桁・数量 10 桁、小数 2 桁)に
        # 収まる 0 以上の有限値だけ受けるためのバリデーション。
        validate_decimal(
            self.unit_price,
            max_digits=12,
            decimal_places=2,
            location="$.parsed_body.items[].unit_price",
        )

msgspecに変えてPydanticを選択することもできるので、そのエコシステムに乗っかれるというのもある。

OpenAPIネイティブ

これもDRFでも実現はできているのだけど、初めから考慮されて実現できているのでOpenAPIのために何かを実装するということが少なくて済む。

DRFだと from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema あたりを使って 個別に指定したりしていたのだが、その辺りがほぼなくなってソースコードが簡潔になった。

DRF(v1):

python manage.py spectacular --file swagger.yaml

DMR(v2):

python manage.py dmr_export_schema utils.api_v2.schema:schema --format json --indent 2 --no-ensure-ascii > swagger-v2.json

YAMLではなくJSONで出力することにしたのは、日本語の説明文をエスケープせずに差分を読めるようにするため。 もしかしたらやり方次第でYAMLでも問題なく出力できるのかもしれない。その辺りは追っていない。

utils.api_v2.schema:schema はv2 routerを読み込ませた from dmr.openapi import build_schema のラッパー:

from dmr.openapi import build_schema
from utils.api_v2.urls import router

schema = build_schema(router)

v1とv2別々に出力して、フロントエンド向けに両方を読み込んで Orval でReactクライアントHooksを生成して使う。 v1の時からこの仕組みでv2のファイル追加して出力先を別にしていた感じです。 フロントエンド側の変更を最小限にするためにOpenAPIのoperation_idを同じにして、v1とv2のパスだけ切り替えれば置き換えができるようにはしておいた。よっぽどエンドポイント名(パス)を変えたいとかがなければそのまま置き換えることができる。

Djangoの資産がそのまま活かせる

Django Admin、ORM、DBマイグレーション、Celry、その他色々そのまま使えるので、移行に際して特に困ることはない。 入れ替えていって最悪v1に戻ることも考えていたくらい。特にその心配はなかったけど。

型

DRFは型がない頃からサポートされていたパッケージなので、不要な型チェックの指摘が大量に出てくる。 ModelSerializerのMetaとかViewSetのなにがしとか。 その辺りがほぼなくなるので、コード読む時も精神が安定している。 今までのDjango REST Frameworkには感謝ですが、django-modern-restの方が開発体験は良いです。

まとめ

という感じです。 Django使ってる方そんなにいないのかな? 使っててモダナイズしたい時、Pythonな新規プロジェクトでFastAPIだと色々考えないとなーって時、ぜひdjango-modern-restをお試しください。