improving-drf-endpoints について
PostHog の Django REST Framework シリアライザーとエンドポイントを改善する開発スキルです。OpenAPI や生成型の問題を調べ、MCP 公開に向けた準備も支援します。
主な機能
- 01シリアライザーフィールドと説明を監査
- 02生成型と API クライアントを確認
- 03ViewSet とカスタムアクションに注釈
- 04型付きレスポンスとエラーを定義
- 05製品のチーム階層ルートを登録
活用シーン
- 01生成型の欠落や誤りを修正
- 02シリアライザーや ViewSet の変更をレビュー
- 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.mdquick-reference-table.mdserializer-fields.mdviewset-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 について、このリストを順に確認してください。
- すべてのフィールドに
help_textがある — 目的、形式、制約、有効な値を説明します - 素の
ListField()やDictField()は使わない — 必ず型付きのシリアライザーまたはフィールドをchild=に指定します - 素の
JSONField()は使わない —@extend_schema_field(TypedSchema)を付けたカスタムフィールドクラスを作成します SerializerMethodFieldには@extend_schema_fieldをそのget_*メソッドに付けるChoiceFieldには明示的なchoices=を付け、すべての有効な値を列挙します- 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 を参照) - 入力の形が出力と異なる場合は、読み取り用と書き込み用のシリアライザーを分ける
- すべての成功レスポンスはシリアライザーに裏付けられている — 生の dict や型なしのリストを返すと、下流に生成される型がなくなります
パターンと例については serializer-fields.md を参照してください。
Viewset とアクションのアノテーション
- すべてのカスタム
@actionに@extend_schemaまたは@validated_requestを付ける — これがないと、drf-spectacular はパラメータを 1 つも検出しません - 素の
ViewSetのメソッドにはスキーマアノテーションを付ける —serializer_classを持つModelViewSetは自動検出されますが、素のViewSetは自動検出されません @extend_schemaは実際のメソッド(get、post、create、list)に付ける — ヘルパーやクラス自体に付けるのではありません- エラーレスポンスには型を付ける —
OpenApiTypes.OBJECTではなくOpenApiResponse(response=ErrorSerializer)を使用します - リストエンドポイントではページネーションを宣言する — ページネーションを行わないカスタムアクションでは
pagination_class=Noneでリセットします - 手動の
serializer.is_valid()+@extend_schemaよりも@validated_requestを優先する — 1 つのデコレーターで両方を処理できます 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 スキャフォールドとフロントエンドの型ジェネレーターはエンドポイントを正しいプロダクトにルーティングできません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— 未解決の衝突を検出し、オーバーライドを提案します