コンテンツへスキップ

クライアントの生成

FastAPIはOpenAPI仕様に基づいているため、自動APIドキュメント(Swagger UIによって提供されます)を含む多くのツールとの自動互換性があります。

必ずしも明白ではない特別な利点の1つは、多くの異なるプログラミング言語用にAPIのクライアントSDKと呼ばれることもあります)を生成できることです。

OpenAPIクライアントジェネレーター

OpenAPIからクライアントを生成するツールはたくさんあります。

一般的なツールはOpenAPI Generatorです。

フロントエンドを構築している場合は、openapi-tsが非常に興味深い代替手段です。

クライアントおよびSDKジェネレーター - スポンサー

OpenAPI(FastAPI)に基づいた企業支援型のクライアントおよびSDKジェネレーターもあります。場合によっては、高品質の生成済みSDK/クライアントに加えて、追加機能を提供することもできます。

それらのいくつかFastAPIをスポンサーしており、これによりFastAPIとそのエコシステムの継続的で健全な開発が保証されます。

そして、それはFastAPIとそのコミュニティ(あなた)への真のコミットメントを示しています。彼らは良いサービスを提供するだけでなく、良い健全なフレームワークであるFastAPIを確実に提供したいと考えているからです。🙇

たとえば、以下を試してみてください。

他にも同様のサービスを提供する企業がいくつかあり、オンラインで検索して見つけることができます。🤓

TypeScriptフロントエンドクライアントの生成

簡単なFastAPIアプリケーションから始めましょう

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
    return {"message": "item received"}


@app.get("/items/", response_model=list[Item])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]
🤓 その他のバージョンとバリアント
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
    return {"message": "item received"}


@app.get("/items/", response_model=List[Item])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]

パス操作が、リクエストペイロードとレスポンスペイロードにItemResponseMessageのモデルを使用していることに注目してください。

APIドキュメント

APIドキュメントにアクセスすると、リクエストで送信され、レスポンスで受信されるデータのスキーマが表示されます。

これらのスキーマは、アプリでモデルと一緒に宣言されているため、見ることができます。

その情報は、アプリのOpenAPIスキーマで利用可能であり、APIドキュメント(Swagger UIによる)に表示されます。

そして、OpenAPIに含まれるモデルからの同じ情報が、クライアントコードを生成するために使用できます。

TypeScriptクライアントの生成

モデルを含むアプリができたので、フロントエンド用のクライアントコードを生成できます。

openapi-tsをインストールする

フロントエンドコードにopenapi-tsをインストールするには、次のようにします。

$ npm install @hey-api/openapi-ts --save-dev

---> 100%

クライアントコードの生成

クライアントコードを生成するには、現在インストールされているコマンドラインアプリケーションopenapi-tsを使用できます。

ローカルプロジェクトにインストールされているため、そのコマンドを直接呼び出すことはできないでしょうが、package.jsonファイルに記述することになります。

次のように見えるかもしれません。

{
  "name": "frontend-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "generate-client": "openapi-ts --input https://:8000/openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "typescript": "^4.6.2"
  }
}

NPMのgenerate-clientスクリプトを作成したら、次のように実行できます。

$ npm run generate-client

frontend-app@1.0.0 generate-client /home/user/code/frontend-app
> openapi-ts --input https://:8000/openapi.json --output ./src/client --client axios

このコマンドは./src/clientにコードを生成し、内部的にaxios(フロントエンドHTTPライブラリ)を使用します。

クライアントコードを試す

これで、クライアントコードをインポートして使用できます。メソッドの自動補完がどのように行われるかに注目してください。

送信するペイロードにも自動補完が適用されます。

ヒント

namepriceの自動補完に注目してください。これらはFastAPIアプリケーションのItemモデルで定義されています。

送信するデータに対してインラインエラーが表示されます。

レスポンスオブジェクトにも自動補完が適用されます。

タグ付きFastAPIアプリ

多くの場合、FastAPIアプリはより大きくなり、さまざまなグループのパス操作を分離するためにタグを使用することになるでしょう。

たとえば、アイテム用のセクションとユーザー用のセクションがあり、それらはタグで区切ることができます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}
🤓 その他のバージョンとバリアント
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}

タグ付きTypeScriptクライアントの生成

タグを使用するFastAPIアプリのクライアントを生成すると、通常、タグに基づいてクライアントコードも分離されます。

これにより、クライアントコードが正しく整理され、グループ化されます。

この場合、次のようなものがあります。

  • ItemsService
  • UsersService

クライアントメソッド名

現在、createItemItemsPostのような生成されたメソッド名はあまりきれいに見えません。

ItemsService.createItemItemsPost({name: "Plumbus", price: 5})

...これは、クライアントジェネレーターが各パス操作のOpenAPI内部の操作IDを使用するためです。

OpenAPIは、すべてのパス操作で各操作IDが一意であることを要求するため、FastAPIは関数名パス、およびHTTPメソッド/操作を使用して操作IDを生成します。これにより、操作IDが一意であることを確認できます。

しかし、次にそれを改善する方法をお見せします。🤓

カスタム操作IDとより良いメソッド名

これらの操作IDが生成される方法を変更して、クライアントでよりシンプルでよりシンプルなメソッド名を持たせることができます。

この場合、各操作IDが何らかの別の方法で一意であることを確認する必要があります。

たとえば、各パス操作にタグがあることを確認し、そのタグとパス操作名前(関数名)に基づいて操作IDを生成できます。

カスタムユニークID生成関数

FastAPIは、各パス操作に対して一意のIDを使用します。これは、操作ID、およびリクエストやレスポンスに必要なカスタムモデルの名前にも使用されます。

この関数はカスタマイズできます。APIRouteを受け取り、文字列を出力します。

例えば、ここでは最初のタグ(おそらくタグは1つだけでしょう)とパス操作の名前(関数名)を使用しています。

次に、そのカスタム関数をgenerate_unique_id_functionパラメーターとしてFastAPIに渡すことができます。

from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel


def custom_generate_unique_id(route: APIRoute):
    return f"{route.tags[0]}-{route.name}"


app = FastAPI(generate_unique_id_function=custom_generate_unique_id)


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}
🤓 その他のバージョンとバリアント
from typing import List

from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel


def custom_generate_unique_id(route: APIRoute):
    return f"{route.tags[0]}-{route.name}"


app = FastAPI(generate_unique_id_function=custom_generate_unique_id)


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}

カスタム操作IDを持つTypeScriptクライアントを生成する

これで、クライアントを再度生成すると、改善されたメソッド名が表示されます。

ご覧のとおり、メソッド名にはタグと関数名が含まれるようになり、URLパスやHTTP操作からの情報は含まれなくなりました。

クライアントジェネレーターのためにOpenAPI仕様を前処理する

生成されたコードにはまだいくつかの重複情報があります。

このメソッドがアイテムに関連していることは、ItemsService(タグから取得)にその単語があるためすでにわかっていますが、メソッド名にもタグ名がプレフィックスとして残っています。😕

OpenAPI全体としてはそれを維持したいでしょう。そうすれば、操作IDの一意性が保証されるからです。

しかし、生成されたクライアントのために、クライアントを生成する直前にOpenAPIの操作IDを修正して、メソッド名をより見やすく、きれいにすることができます。

OpenAPI JSONをopenapi.jsonファイルにダウンロードし、次のようなスクリプトでプレフィックスされたタグを削除できます。

import json
from pathlib import Path

file_path = Path("./openapi.json")
openapi_content = json.loads(file_path.read_text())

for path_data in openapi_content["paths"].values():
    for operation in path_data.values():
        tag = operation["tags"][0]
        operation_id = operation["operationId"]
        to_remove = f"{tag}-"
        new_operation_id = operation_id[len(to_remove) :]
        operation["operationId"] = new_operation_id

file_path.write_text(json.dumps(openapi_content))
import * as fs from 'fs'

async function modifyOpenAPIFile(filePath) {
  try {
    const data = await fs.promises.readFile(filePath)
    const openapiContent = JSON.parse(data)

    const paths = openapiContent.paths
    for (const pathKey of Object.keys(paths)) {
      const pathData = paths[pathKey]
      for (const method of Object.keys(pathData)) {
        const operation = pathData[method]
        if (operation.tags && operation.tags.length > 0) {
          const tag = operation.tags[0]
          const operationId = operation.operationId
          const toRemove = `${tag}-`
          if (operationId.startsWith(toRemove)) {
            const newOperationId = operationId.substring(toRemove.length)
            operation.operationId = newOperationId
          }
        }
      }
    }

    await fs.promises.writeFile(
      filePath,
      JSON.stringify(openapiContent, null, 2),
    )
    console.log('File successfully modified')
  } catch (err) {
    console.error('Error:', err)
  }
}

const filePath = './openapi.json'
modifyOpenAPIFile(filePath)

これにより、操作IDはitems-get_itemsのようなものから単にget_itemsに名前が変更され、クライアントジェネレーターがよりシンプルなメソッド名を生成できるようになります。

前処理されたOpenAPIを持つTypeScriptクライアントを生成する

最終結果がファイルopenapi.jsonにあるので、例えば、package.jsonを編集してそのローカルファイルを使用するように変更します。

{
  "name": "frontend-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "typescript": "^4.6.2"
  }
}

新しいクライアントを生成すると、すべての自動補完インラインエラーなどとともに、きれいなメソッド名が得られます。

利点

自動生成されたクライアントを使用すると、以下の自動補完が得られます。

  • メソッド。
  • 本文、クエリパラメーターなどのリクエストペイロード。
  • レスポンスペイロード。

すべての項目についてインラインエラーも表示されます。

そして、バックエンドコードを更新し、フロントエンドを再生成するたびに、新しいパス操作がメソッドとして利用可能になり、古いものは削除され、その他の変更も生成されたコードに反映されます。🤓

これは、何かが変更された場合、それがクライアントコードに自動的に反映されることも意味します。そして、クライアントをビルドすると、使用されているデータに不一致がある場合、エラーが発生します。

したがって、多くのエラーを開発サイクルの非常に早い段階で検出でき、本番環境で最終ユーザーにエラーが表示されるまで待ってから、問題がどこにあるかをデバッグしようとする必要がなくなります。✨