mcpField
I
improving-drf-endpoints

improving-drf-endpoints

github.com/posthog開発40.5k201 回インストール今日更新
リポジトリ

improving-drf-endpoints について

PostHog の Django REST Framework シリアライザーとエンドポイントを改善する開発スキルです。OpenAPI や生成型の問題を調べ、MCP 公開に向けた準備も支援します。

主な機能

  1. 01シリアライザーフィールドと説明を監査
  2. 02生成型と API クライアントを確認
  3. 03ViewSet とカスタムアクションに注釈
  4. 04型付きレスポンスとエラーを定義
  5. 05製品のチーム階層ルートを登録

活用シーン

  1. 01生成型の欠落や誤りを修正
  2. 02シリアライザーや ViewSet の変更をレビュー
  3. 03MCP ツール向けエンドポイントを準備

MCP 対応

生成される MCP ツールのスキーマに影響するエンドポイントとシリアライザーの変更を扱い、ツール作成の次段階として implementing-mcp-tools スキルを案内します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/posthog/posthog --skill improving-drf-endpoints

インストール後、Claude Code で/improving-drf-endpointsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
common-anti-patterns.md
quick-reference-table.md
serializer-fields.md
viewset-annotations.md

詳細

DRF エンドポイントの改善

生成された OpenAPI スキーマに対するコントラクトテストを提案する前に、すでに試されたことを確認してください。6 つの PR がそのアイデアを採用しましたが、いずれもマージされませんでした。

概要

シリアライザーのフィールドは、PostHog の型パイプライン全体における信頼できる唯一の情報源です。

Django serializer → drf-spectacular → OpenAPI JSON → Orval → Zod schemas → MCP tools

すべての help_text、すべてのフィールド型、すべての @extend_schema アノテーションが下流に流れていきます。 help_text が欠けていると、エージェントがパラメータを推測することになります。 素の ListField() は、生成される Zod スキーマにおける z.unknown() を意味します。 シリアライザーを正しく作ることで、すべての利用者 — フロントエンドの型、MCP ツール、API ドキュメント — が正しい型と説明を自動的に得られます。

使用すべき場面

  • Serializer または ViewSet を定義するファイルを編集またはレビューするとき
  • OpenAPI スペックの警告や生成された型の問題を修正するとき
  • エンドポイントを MCP ツールとして公開する準備をするとき
  • API 変更のコードレビュー

監査チェックリスト

トリアージ: まず生成された出力を確認する

Python を掘り下げる前に、コミットされた生成済みの型を見て、何が壊れているかを確認してください。 エンドポイントのプロダクトに対応する生成ファイルを探します:

  • コア API: frontend/src/generated/core/
  • プロダクト API: products/<product>/frontend/generated/

それぞれに 2 つのファイルがあります:

  • api.schemas.ts — シリアライザーから導出された TypeScript インターフェース。シリアライザー名を検索し、unknown 型(素の ListField/JSONField)、JSDoc の説明の欠落(help_text の欠落)、または過度に汎用的な Record<string, unknown> の形を探します。
  • api.ts — API クライアント関数。エンドポイントのオペレーションがそもそも存在するかを確認します — 存在しない場合、viewset のメソッドに @extend_schema が欠けている可能性が高いです。

これにより、どのフィールドとエンドポイントを優先すべきかが正確にわかります。

シリアライザーのフィールド

触れるすべてのシリアライザーと viewset について、このリストを順に確認してください。

  1. すべてのフィールドに help_text がある — 目的、形式、制約、有効な値を説明します
  2. 素の ListField() や DictField() は使わない — 必ず型付きのシリアライザーまたはフィールドを child= に指定します
  3. 素の JSONField() は使わない — @extend_schema_field(TypedSchema) を付けたカスタムフィールドクラスを作成します
  4. SerializerMethodField には @extend_schema_field をその get_* メソッドに付ける
  5. ChoiceField には明示的な choices= を付け、すべての有効な値を列挙します
  6. choices はクラスとして定義する — プロダクトの内部モジュールでは models.TextChoices を、ファサードのコントラクトファイル(backend/facade/contracts.py、backend/facade/enums.py)では posthog/enums.py の LabeledStrEnum または LabeledIntEnum を使います。後者は Django をインポートしてはなりません。OpenAPI のコンポーネントはどちらの場合もクラス名にちなんで命名されるため(EarlyAccessFeature.Stage -> EarlyAccessFeatureStageEnum、posthog/openapi/enum_names.py の ChoicesEnumNameOverrides 経由)、クラスに基づく enum は format、type、status、kind のようなフィールド名で衝突することはありません。choices= には X.choices を渡し、SerializerMethodField 上のラベル付き enum に名前を付けるには、戻り値の型ヒントではなく @extend_schema_field(ChoiceField(choices=X.choices)) を使います。インラインの choices=[...] リストには読み取るべきクラスがなく、既存の choices と衝突し、--fail-on-warn で CI に失敗します。どのクラスも担えない choice セットに対するフォールバックは、posthog/settings/web.py の明示的な ENUM_NAME_OVERRIDES エントリです(serializer-fields.md を参照)
  7. 入力の形が出力と異なる場合は、読み取り用と書き込み用のシリアライザーを分ける
  8. すべての成功レスポンスはシリアライザーに裏付けられている — 生の dict や型なしのリストを返すと、下流に生成される型がなくなります

パターンと例については serializer-fields.md を参照してください。

Viewset とアクションのアノテーション
  1. すべてのカスタム @action に @extend_schema または @validated_request を付ける — これがないと、drf-spectacular はパラメータを 1 つも検出しません
  2. 素の ViewSet のメソッドにはスキーマアノテーションを付ける — serializer_class を持つ ModelViewSet は自動検出されますが、素の ViewSet は自動検出されません
  3. @extend_schema は実際のメソッド(get、post、create、list)に付ける — ヘルパーやクラス自体に付けるのではありません
  4. エラーレスポンスには型を付ける — OpenApiTypes.OBJECT ではなく OpenApiResponse(response=ErrorSerializer) を使用します
  5. リストエンドポイントではページネーションを宣言する — ページネーションを行わないカスタムアクションでは pagination_class=None でリセットします
  6. 手動の serializer.is_valid() + @extend_schema よりも @validated_request を優先する — 1 つのデコレーターで両方を処理できます
  7. products/ の外にある ViewSet には @extend_schema(extensions={"x-product": "<product>"}) が必要 — products/<name>/backend/ 内の ViewSet はモジュールパス経由で自動的に属性付けされますが、posthog/api/ や ee/ 内の ViewSet はそうではなく、x-product 拡張で属性を明示的に宣言する必要があります。プレーンな文字列("product_analytics")または ProductKey.X 列挙型(ケバブケースの値は正規化されます)を受け付けます。コード生成のルーティングに影響を与えるために tags=["<product>"] を使わないでください — tags は Swagger UI の表示専用です。x-product がないと、MCP スキャフォールドとフロントエンドの型ジェネレーターはエンドポイントを正しいプロダクトにルーティングできません
  8. partial_update の request= オーバーライドは、実行時の書き込みフィールドのスーパーセットでなければならない — extend_schema(request=CustomSerializer) は serializer_class からの drf-spectacular の推論を置き換えます。省略されたフィールドは、実行時のシリアライザーが依然として受け付ける場合でも、OpenAPI、フロントエンドの型、MCP ツールスキーマから消えます。オーバーライドを変更した後は、hogli build:openapi を実行し、生成された MCP ツールスキーマがすべての OpenAPI ボディフィールドを引き続き公開していることを確認してください

ストリーミングエンドポイント: SSE またはストリーミングレスポンスの場合、@extend_schema(request=InputSerializer, responses={(200, "text/event-stream"): OpenApiTypes.STR}) を使用して、レスポンスに完全な型を付けられなくてもリクエストスキーマを文書化します。

パターンと例については viewset-annotations.md を参照してください。

URL ルーティング — チームにネストされた新しいエンドポイントをどこに登録するか

PostHog は一時的にプロジェクトと環境を別々の概念に分割しましたが、その後その分割を撤回しました。/api/projects/:team_id/... が、チームにネストされたあらゆるエンドポイントの正規パスです。/api/environments/:team_id/... は、分割中にそれに対して統合したクライアントのためだけに保持されている後方互換のエイリアスです。

新規のチームにネストされたエンドポイントは、routers.projects の下に登録します。ルートは各プロダクト自身の products/<name>/backend/routes.py 内の register_routes(routers) 関数に置きます:

# products/<name>/backend/routes.py
from posthog.api.routing import RouterRegistry


def register_routes(routers: RouterRegistry) -> None:
    routers.projects.register(r"my_thing", MyThingViewSet, "project_my_thing", ["team_id"])

プロダクトのルートは自動検出されます — posthog/api/__init__.py が INSTALLED_APPS を反復処理し、routes.py を持つすべての products.* アプリで register_routes(routers) を呼び出します。プロダクトの追加にコアの編集は不要です: products/<name>/backend/routes.py を作成し、そのプロダクトが PRODUCTS_APPS(posthog/settings/web.py) に含まれていることを確認してください。プロダクトではないコアのビューセットのみが、いまだに __init__.py に直接登録されます。

なぜコアがプロダクトを検出して呼び出すのか(プロダクトがコアを呼び出すのではない)。 コアはまず 4 つの親(root + projects/environments/organizations)を登録し、その後で検出ループを実行します。プロダクトはこれらの親にのみネストし、互いにネストすることはないため、検出順序は関係ありません。登録は eager に保たれており(posthog.api が最初にインポートされたとき、つまり最初のリクエスト時に実行されます)、意図的に AppConfig.ready() には移動されて_いません_: ready() はすべてのプロセスで django.setup() の内部で実行され、ルートの登録はそのビューセットをインポートするため、あらゆる場所で API 全体を setup() に引き込んでしまうことになります — これは Celery ワーカーや管理コマンドから API を締め出す遅延性を損なうことになります。詳細な理由については、RouterRegistry の docstring と posthog/api/__init__.py 内の検出ループを参照してください。

チームにネストされたエンドポイントは、project_<name> ベースネームを付けて routers.projects の下に登録します。environments_router も、デュアルルートヘルパーも存在しません: レガシーな /api/environments/* サーフェスは、登録済みルートの集合としては廃止されました。

いまだに /api/environments/... を呼び出す既存のクライアントは、EnvironmentsRewriteMiddleware によって透過的に処理されます。これは、パスをプロセス内で等価な /api/projects/* ビューセットに書き換えます(307 はなし)。このために env ルートを登録することは決してありません — routers.projects の下に登録するだけで、ミドルウェアがエイリアスを処理します。

ファサードプロダクト(DataclassSerializer)

contracts.py の frozen データクラスをラップする DataclassSerializer を使うファサードパターンのプロダクト(例: visual_review)の場合:

  • フィールド型はデータクラスから自動導出されます — 設計上、型付けの問題が少なくなります
  • 型の enum フィールドは、プレーンな StrEnum ではなく LabeledStrEnum/LabeledIntEnum(または Django の Choices クラス)として指定します。 posthog/api/documentation.py のスキーマ拡張が、これらのフィールドをそのクラスラベル付きで文書化するため、その OpenAPI enum はクラス名にちなんで命名されます。プレーンな StrEnum はフィールド由来の名前にフォールバックします。
  • help_text に注力してください(データクラスのフィールドはそれを持ちません。シリアライザーのフィールドオーバーライドで追加してください)
  • @validated_request はすでに標準パターンです — レスポンスシリアライザーが宣言されていることを確認してください
  • @extend_schema のタグと説明は、いまだにビューセットのメソッドで設定する必要があります

判断フローチャート

digraph audit {
    rankdir=TB
    node [shape=diamond fontsize=10]
    edge [fontsize=9]

    start [label="Serializer or\nViewSet file?" shape=box]
    is_model [label="ModelViewSet with\nserializer_class?"]
    is_plain [label="Plain ViewSet or\ncustom @action?"]
    is_facade [label="DataclassSerializer\n(facade product)?"]

    check_fields [label="Check fields:\nhelp_text, ListField,\nJSONField, ChoiceField" shape=box]
    add_schema [label="Add @validated_request\nor @extend_schema to\nevery method" shape=box]
    check_help [label="Focus on help_text\nand response declarations" shape=box]
    check_responses [label="Check response types,\npagination, error schemas" shape=box]

    start -> is_model
    is_model -> check_fields [label="yes"]
    is_model -> is_plain [label="no"]
    is_plain -> add_schema [label="yes"]
    is_plain -> is_facade [label="no"]
    is_facade -> check_help [label="yes"]
    check_fields -> check_responses
    add_schema -> check_fields
    check_help -> check_responses
}

クイックリファレンス

ざっと目を通せる「X を見たら Y をする」の参照表については、quick-reference-table.md を参照してください。

ビフォー/アフターのコードペアについては、common-anti-patterns.md を参照してください。

コードベース内の正規の例

  • JSONField + @extend_schema_field: products/alerts/backend/presentation/views/alert.py
  • @validated_request: products/tasks/backend/presentation/views/api.py
  • help_text + 型付きレスポンス: products/ai_observability/backend/api/summarization.py
  • ファサードプロダクト: products/visual_review/backend/presentation/views.py

関連

  • ダウンストリーム: シリアライザーを修正した後、implementing-mcp-tools スキルを使用して MCP ツールをスキャフォールドします
  • パイプラインのドキュメント: docs/published/handbook/engineering/type-system.md
  • ミックスイン: posthog/api/mixins.py(@validated_request のソース)
  • drf-spectacular の設定: posthog/settings/web.py(SPECTACULAR_SETTINGS)
  • Enum 衝突の診断: python manage.py find_enum_collisions — 未解決の衝突を検出し、オーバーライドを提案します

コメント