Python StrawberryとFastAPIによるGraphQL APIの構築:スキーマ設計から認証まで

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

RESTのループを打破する:エンドポイントが制御不能に増殖するとき

2021年のダッシュボードプロジェクトを思い出します。フロントエンドの要件が毎週のように変わっていました。あるビューではユーザーの詳細と直近5件の注文が必要になり、別のビューでは同じ詳細に加えて、有効なサブスクリプションと支払い設定が必要になりました。1か月もしないうちに、私たちのREST API/user-with-orders/user-full-profile-v2 といったエンドポイントで溢れかえりました。それはまさにメンテナンスの負債という罠でした。

これは古典的なRESTの課題です。データをオーバーフェッチ(過剰取得)して200ミリ秒で済むページ読み込みを3G接続での2秒間の苦行に変えてしまうか、あるいはアンダーフェッチ(不足取得)してフロントエンドにヘッダー1つを表示するためだけに5つの別々のネットワーク呼び出しを強いるかのどちらかです。GraphQLの習得はもはや選択肢ではなく、自重で崩壊しないインターフェースを構築するための必須条件となっています。

アーキテクチャの乖離:リソース vs コンポーネント

この摩擦は、根本的なミスマッチから生じています。RESTはリソース指向であるのに対し、現代のUIはコンポーネント駆動です。RESTのURLは固定されたJSON構造を返します。しかし、サイドバーコンポーネントにはユーザー名とアバターだけで十分な場合もあれば、設定ページには完全な支払い履歴とセキュリティログが必要な場合もあります。

動的なUIに対して硬直したリソース構造を強いることは、オーバーヘッドを生みます。多くのチームは、BFF(Backends for Frontends)を構築したり、コントローラーに ?include=orders,prefs&fields=id,name のような煩雑なクエリパラメータを詰め込んだりすることで解決しようとします。このアプローチは、本質的にはGraphQLの脆弱で手動な模倣に過ぎず、組み込みの型安全やエコシステムの恩恵を全く受けることができません。

Pythonのランドスケープ:Graphene vs Strawberry

長年、PythonにおけるGraphQLの標準ライブラリは Graphene でした。2015年頃にリリースされ、その役割を果たしてきましたが、次第に古臭さが目立つようになりました。マジックストリングや、現代のPythonの型ヒントとしばしば衝突するカスタムクラスに依存していたためです。そこに Strawberry 登場しました。

Strawberryは、Pythonのdataclassと型ヒントの上にネイティブに構築されています。FastAPIやPydanticを使っているなら、その構文は既知のものでしょう。 typing モジュールを活用して、GraphQLスキーマを自動的に生成します。PythonコードがAPIコントラクトの「信頼できる唯一の情報源(Single Source of Truth)」になります。従来のライブラリと比較して、Strawberryは優れたIDE補完を提供し、本番環境にデプロイする前の型チェック段階でバグをキャッチできます。

戦略:型安全なスタックの構築

StrawberryとFastAPIを組み合わせることで、強力なスタックが完成します。FastAPIの電光石火の非同期パフォーマンスと、Strawberryの開発者フレンドリーなスキーマ定義の両方を享受できます。具体的な実装を見ていきましょう。

1. 基盤の設定

まずは環境の準備から始めます。 fastapiuvicorn、そして strawberry-graphql[fastapi] が必要です。

pip install fastapi uvicorn 'strawberry-graphql[fastapi]'

2. スキーマの設計

独立した .graphql ファイルの管理は不要です。ロジックと型を同期させるため、すべてをPythonで定義します。

import strawberry
from typing import List, Optional

@strawberry.type
class Book:
    id: strawberry.ID
    title: str
    author: str

@strawberry.type
class User:
    id: strawberry.ID
    username: str
    email: str
    
    @strawberry.field
    def books(self) -> List[Book]:
        # 本番環境では、ここがデータベースレイヤーと接続されます
        return [
            Book(id=strawberry.ID("1"), title="Clean Code", author="Robert C. Martin")
        ]

3. リゾルバーの作成

リゾルバーはAPIのエンジンです。Strawberryでは、単純なメソッドやスタンドアロン関数として定義します。 Query クラスがすべての読み取りリクエストのゲートウェイとして機能します。

@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: strawberry.ID) -> Optional[User]:
        # ユーザー取得のロジックをここに記述します
        return User(id=id, username="johndoe", email="[email protected]")

schema = strawberry.Schema(query=Query)

4. FastAPIとの統合

StrawberryスキーマをFastAPIのルートにマウントするのは非常に簡単です。このセットアップにより、 /graphql でGraphiQL IDEがすぐに利用可能になり、追加設定なしでクエリをテストできます。

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

app = FastAPI()
graphql_app = GraphQLRouter(schema)

app.include_router(graphql_app, prefix="/graphql")

認証:リゾルバーの罠を避ける

よくある間違いは、個々のリゾルバーすべてに認証ロジックを記述してしまうことです。これはセキュリティホールの巨大な温床となります。代わりに Context を使いましょう。FastAPIの依存関係(Dependency)でユーザーの身元を抽出し、それをGraphQLコンテキストに一度だけ注入します。

from strawberry.fastapi import BaseContext
from fastapi import Depends, Request

class CustomContext(BaseContext):
    def __init__(self, user_id: Optional[str]):
        self.user_id = user_id

async def get_context(request: Request):
    # 例:JWTやヘッダーの解析
    user_id = request.headers.get("Authorization")
    return CustomContext(user_id=user_id)

graphql_app = GraphQLRouter(schema, context_getter=get_context)

これで、リゾルバーから info.context.user_id にアクセスできるようになります。認可処理は関数の冒頭でのシンプルなチェックに簡略化されます。

@strawberry.field
def private_data(self, info: strawberry.Info) -> str:
    if not info.context.user_id:
        raise Exception("認証されていません")
    return "これは制限されたコンテンツです"

パフォーマンス:N+1問題の解決

20人のユーザーを取得し、それぞれに対して本を取得するといったクエリのネストは、「N+1問題」を引き起こす可能性があります。アプリはユーザー取得のために1回のリクエストを行い、その後の本の取得のために20回の別々の、逐次的なリクエストを行います。これはパフォーマンスを著しく低下させます。

Strawberryの DataLoader は、これら20回のリクエストを1つのデータベースクエリにバッチ化(一括化)することで解決します。本番環境では、外部キー関係に対してDataLoaderの使用は避けて通れません。これこそが、きびきびとしたインターフェースと、負荷がかかると這いつくばるようなAPIの分かれ目となります。

デプロイの現実

本番環境へ移行する前に、スキーマ全体を公開したくない場合はGraphiQLインターフェースを無効にしてください。環境変数を使用して、 GraphQLRoutergraphql_ide=None に切り替えます。デプロイは標準的です。Dockerコンテナ内で uvicorn を使用するか、マルチコアスケーリングのために uvicorn.workers.UvicornWorker クラスを指定して gunicorn を使用します。

RESTからStrawberryへの移行は、単なる構文の変更ではありません。バックエンドチームとフロントエンドチームの間の契約(コントラクト)を明確にします。フロントエンドが必要なものだけを正確に要求するようになれば、バグのカテゴリー全体を排除でき、APIの形状について議論する会議の時間も大幅に短縮できます。

Share: