クライアントの生成¶
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 http://localhost: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 http://localhost: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
(タグから取得) にその単語が含まれているため、アイテムに関連していることがすでにわかっていますが、メソッド名にもタグ名がプレフィックスとして付いています。😕
操作 ID が一意であることを保証するため、OpenAPI では一般的にそのままにしておく必要があります。
しかし、生成されたクライアントの場合、クライアントを生成する直前に 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"
}
}
新しいクライアントを生成した後、すべての自動補完、インラインエラーなどを備えたクリーンなメソッド名が得られます。
利点¶
自動生成されたクライアントを使用すると、以下に対して自動補完が得られます。
- メソッド。
- 本文、クエリパラメータなどのリクエストペイロード。
- レスポンスペイロード。
また、すべてにインラインエラーが表示されます。
バックエンドコードを更新し、フロントエンドを再生成するたびに、新しいパス操作がメソッドとして使用可能になり、古いものが削除され、その他の変更が生成されたコードに反映されます。🤓
これは、何か変更された場合、クライアントコードに自動的に反映されることを意味します。また、クライアントをビルドすると、使用されているデータに不一致がある場合、エラーが発生します。
そのため、最終ユーザーに本番環境でエラーが表示されてから問題の場所をデバッグしようとするのではなく、開発サイクルの非常に早い段階で多くのエラーを検出できます。✨