クライアントの生成¶
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},
]
パス操作が、リクエストペイロードとレスポンスペイロードにItem
とResponseMessage
のモデルを使用していることに注目してください。
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ライブラリ)を使用します。
クライアントコードを試す¶
これで、クライアントコードをインポートして使用できます。メソッドの自動補完がどのように行われるかに注目してください。
送信するペイロードにも自動補完が適用されます。
ヒント
name
とprice
の自動補完に注目してください。これらは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"
}
}
新しいクライアントを生成すると、すべての自動補完、インラインエラーなどとともに、きれいなメソッド名が得られます。
利点¶
自動生成されたクライアントを使用すると、以下の自動補完が得られます。
- メソッド。
- 本文、クエリパラメーターなどのリクエストペイロード。
- レスポンスペイロード。
すべての項目についてインラインエラーも表示されます。
そして、バックエンドコードを更新し、フロントエンドを再生成するたびに、新しいパス操作がメソッドとして利用可能になり、古いものは削除され、その他の変更も生成されたコードに反映されます。🤓
これは、何かが変更された場合、それがクライアントコードに自動的に反映されることも意味します。そして、クライアントをビルドすると、使用されているデータに不一致がある場合、エラーが発生します。
したがって、多くのエラーを開発サイクルの非常に早い段階で検出でき、本番環境で最終ユーザーにエラーが表示されるまで待ってから、問題がどこにあるかをデバッグしようとする必要がなくなります。✨