Litestar で OpenAPI タグを効率的に管理する方法
Litestar で API を構築する際、OpenAPI ドキュメント生成のためにタグを定義する場面が多くあります。しかし、タグの定義方法について「どこに書くべきか」「どう一元管理するか」という疑問が生じることがあります。
本記事では、Litestar におけるタグ管理の私的ベストプラクティスを紹介します。
Litestar のタグの役割は2つに分かれている
Litestar では、タグに関して以下の2つの役割があります。
-
ルート側の
tags: そのエンドポイント(operation)がどのタグに属するかを示す -
OpenAPIConfigのtags: タグ自体のメタデータ(説明文、外部ドキュメントへのリンクなど)を定義
つまり、同じタグ名を「API側」と「OpenAPIConfig側」の両方で書く場面があるということです。
実務的な考え方
この2つの役割を整理すると、以下のように考えるのが最も分かりやすいです。
- タグの所属は各 handler / controller / router 側で付ける
- タグの説明文や外部ドキュメントは
OpenAPIConfig.tagsにまとめる - タグ名の文字列をバラ撒かないために
StrEnumを single source of truth にするのがベストプラクティス
ベストプラクティス
1. タグ名は StrEnum で一元管理する
まず、タグ名を StrEnum で定義します。これにより、タグ名の typo を防ぎ、IDE の補完機能を活用できます。
from enum import StrEnum
class ApiTag(StrEnum):
USERS = "users"
ITEMS = "items"
INTERNAL = "internal"
このエニュムを route 側でも OpenAPI 側でも使用します。
2. ルート側では value を使って紐付ける
Litestar のルート、コントローラー、アプリケーション側の tags は文字列のリストです。明示的に .value を使うことで、API 定義として最終的に文字列になることを保証します。
from litestar import get
@get("/users", tags=[ApiTag.USERS.value])
async def list_users() -> list[dict]:
return []
3. OpenAPIConfig 側では Tag を enum から生成する
OpenAPIConfig 側では、enum から Tag オブジェクトを生成します。
from litestar import Litestar
from litestar.openapi import OpenAPIConfig
from litestar.openapi.spec import Tag, ExternalDocumentation
OPENAPI_TAGS = [
Tag(
name=ApiTag.USERS.value,
description="ユーザー関連API",
),
Tag(
name=ApiTag.ITEMS.value,
description="商品関連API",
),
Tag(
name=ApiTag.INTERNAL.value,
description="内部向けAPI",
external_docs=ExternalDocumentation(
description="内部設計資料",
url="https://example.com/internal-api-docs",
),
),
]
app = Litestar(
route_handlers=[list_users],
openapi_config=OpenAPIConfig(
title="My API",
version="1.0.0",
tags=OPENAPI_TAGS,
),
)
こうしておくと、ルート側のタグ名と OpenAPI 上のタグ定義が必ず一致します。
さらにおすすめの形:enum + メソッド
Enum だけだと description を別管理しがちなので、少し踏み込むなら enum にメソッドを追加するのが扱いやすいです。
from enum import StrEnum
from litestar.openapi.spec import Tag
class ApiTag(StrEnum):
USERS = "users"
ITEMS = "items"
INTERNAL = "internal"
@property
def description(self) -> str:
return {
ApiTag.USERS: "ユーザー関連API",
ApiTag.ITEMS: "商品関連API",
ApiTag.INTERNAL: "内部向けAPI",
}[self]
def as_openapi_tag(self) -> Tag:
return Tag(name=self.value, description=self.description)
openapi_config = OpenAPIConfig(
title="My API",
version="1.0.0",
tags=[tag.as_openapi_tag() for tag in ApiTag],
)
ルート側は変わりません。
@get("/users", tags=[ApiTag.USERS.value])
async def list_users() -> list[dict]:
return []
この形だと、タグ追加時に定義漏れが起きにくくなります。
「両方に登録しないといけないのか?」について
厳密には、用途が異なるため「同じものを二重管理している」というより、以下のように考えるべきです。
- ルート側: このAPIはどのタグに属するか
- OpenAPIConfig.tags: そのタグは何者か(説明文など)
そのため、description など不要なら OpenAPIConfig.tags を省く運用もあります。ただし、Swagger / Scalar / ReDoc 上でタグ説明をきれいに出したいなら、OpenAPIConfig.tags も持っておく価値があります。
推奨される実装パターン
最終的には、以下のパターンが最も無難です。
-
StrEnumでタグ名を定義 - ルート側は
tags=[ApiTag.X.value] -
OpenAPIConfig.tagsは enum から自動生成 - description / external docs も enum 側に寄せる
つまり、**「二重登録」ではなく「単一ソースから2箇所へ展開」**にする、という設計です。
最後に
Litestar でタグを管理する際、この方法を採用することで以下のメリットが得られます。
- タグ名の typo を防ぐ
- IDE の補完機能を活用できる
- 定義漏れを防ぐ
- 保守性が向上する
ただし、プロジェクトの規模や要件によって、最適な実装方法は異なるかもしれません。
皆さんは、実務で Litestar のタグをどのように管理していますか?より良い方法や工夫があれば、ぜひコメントで教えてください!