ReactからFastAPIへ
api-connection
ジェネレータは、ReactウェブサイトとFastAPIバックエンドを迅速に統合する方法を提供します。タイプセーフな方法でFastAPIバックエンドに接続するために必要なすべての設定(クライアントとTanStack Queryフックの生成、AWS IAM認証のサポート、適切なエラーハンドリングを含む)を行います。
前提条件
このジェネレータを使用する前に、Reactアプリケーションが以下を備えていることを確認してください:
- アプリケーションをレンダリングする
main.tsx
ファイル - 動作するFastAPIバックエンド(FastAPIジェネレータで生成されたもの)
必要なmain.tsx
の構造例
import { StrictMode } from 'react';import * as ReactDOM from 'react-dom/client';import App from './app/app';
const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement,);root.render( <StrictMode> <App /> </StrictMode>,);
使用方法
ジェネレータの実行
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - api-connection
- 必須パラメータを入力
- クリック
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
変更されるファイルを確認するためにドライランを実行することもできます
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
オプション
パラメータ | 型 | デフォルト | 説明 |
---|---|---|---|
sourceProject 必須 | string | - | The source project which will call the API |
targetProject 必須 | string | - | The target project containing your API |
auth | string | IAM | Authentication strategy (choose from IAM or None) |
ジェネレータの出力
ジェネレータはFastAPIプロジェクトの以下のファイルを変更します:
Directoryscripts
- generate_open_api.py APIのOpenAPI仕様を生成するスクリプトを追加
- project.json 上記の生成スクリプトを呼び出す新しいビルドターゲットを追加
ジェネレータはReactアプリケーションの以下のファイルを変更します:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx APIクライアントのプロバイダ
- QueryClientProvider.tsx TanStack React Queryクライアントプロバイダ
Directoryhooks
- use<ApiName>.tsx TanStack Queryで状態管理されたAPI呼び出し用フックを追加
- use<ApiName>Client.tsx 直接APIを呼び出すバニラクライアント生成用フックを追加
- useSigV4.tsx IAM認証選択時にHTTPリクエストのSigV4署名用フックを追加
- project.json タイプセーフなクライアントを生成する新しいビルドターゲットを追加
- .gitignore 生成されたクライアントファイルをデフォルトで無視
また、ジェネレータはウェブサイトインフラにRuntime Configを追加します(まだ存在しない場合)。これにより、FastAPIのAPI URLがウェブサイトで利用可能になり、use<ApiName>.tsx
フックによって自動的に設定されます。
コード生成
ビルド時に、FastAPIのOpenAPI仕様からタイプセーフなクライアントが生成されます。これによりReactアプリケーションに3つの新しいファイルが追加されます:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts FastAPIで定義されたpydanticモデルから生成された型
- client.gen.ts API呼び出し用タイプセーフクライアント
- options-proxy.gen.ts TanStack Queryを使用したAPI操作のためのフックオプション生成メソッドを提供
生成コードの使用方法
生成されたタイプセーフクライアントを使用して、ReactアプリケーションからFastAPIを呼び出すことができます。TanStack Queryフック経由での使用が推奨されますが、直接クライアントを使用することも可能です。
APIフックの使用
ジェネレータはuse<ApiName>
フックを提供し、TanStack QueryでAPIを呼び出すことができます。
クエリ
queryOptions
メソッドを使用して、TanStack QueryのuseQuery
フックでAPIを呼び出すためのオプションを取得できます:
import { useQuery } from '@tanstack/react-query';import { useState, useEffect } from 'react';import { useMyApi } from './hooks/useMyApi';
function MyComponent() { const api = useMyApi(); const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
if (item.isLoading) return <div>Loading...</div>; if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}
APIクライアントの直接使用
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}
ミューテーション
生成されたフックには、TanStack QueryのuseMutation
フックを使用したミューテーションサポートが含まれます。これにより、ローディング状態、エラーハンドリング、楽観的更新を備えた作成・更新・削除操作が可能になります。
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // 生成されたミューテーションオプションを使用 const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* フォームフィールド */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? '作成中...' : 'アイテム作成'} </button>
{createItem.isSuccess && ( <div className="success"> 作成されたアイテムID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> エラー: {createItem.error.message} </div> )} </form> );}
ミューテーション状態ごとにコールバックを追加できます:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { console.log('アイテム作成:', data); navigate(`/items/${data.id}`); }, onError: (error) => { console.error('アイテム作成失敗:', error); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
クライアント直接使用のミューテーション例
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); } catch (err) { setError(err); console.error('アイテム作成失敗:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* フォームフィールド */} <button type="submit" disabled={isLoading} > {isLoading ? '作成中...' : 'アイテム作成'} </button>
{createdItem && ( <div className="success"> 作成されたアイテムID: {createdItem.id} </div> )}
{error && ( <div className="error"> エラー: {error.message} </div> )} </form> );}
無限クエリによるページネーション
cursor
パラメータを受け入れるエンドポイントに対して、生成されたフックはTanStack QueryのuseInfiniteQuery
フックを使用した無限クエリをサポートします。「さらに読み込む」や無限スクロール機能の実装が容易になります。
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // 1ページあたりのアイテム数 }, { getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> <ul> {items.data.pages.flatMap(page => page.items.map(item => ( <li key={item.id}>{item.name}</li> )) )} </ul>
<button onClick={() => items.fetchNextPage()} disabled={!items.hasNextPage || items.isFetchingNextPage} > {items.isFetchingNextPage ? 'さらに読み込み中...' : items.hasNextPage ? 'さらに読み込む' : 'これ以上ありません'} </button> </div> );}
生成されたフックは、APIがサポートする場合のカーソルベースページネーションを自動処理します。nextCursor
値はレスポンスから抽出され、次のページの取得に使用されます。
クライアント直接使用のページネーション例
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? '読み込み中...' : nextCursor ? 'さらに読み込む' : 'これ以上ありません'} </button> </div> );}
エラーハンドリング
統合には型指定されたエラーレスポンスが含まれます。OpenAPI仕様で定義された可能なエラーレスポンスをカプセル化する<operation-name>Error
型が生成されます。各エラーはstatus
とerror
プロパティを持ち、status
値をチェックすることで特定のエラー種別を判別できます。
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: 'New Item' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: return ( <div> <h2>無効な入力:</h2> <p>{createItem.error.error.message}</p> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>権限なし:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>サーバーエラー:</h2> <p>{createItem.error.error.message}</p> <p>トレースID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>アイテム作成</button>;}
クライアント直接使用のエラーハンドリング例
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: return ( <div> <h2>無効な入力:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>権限なし:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>サーバーエラー:</h2> <p>{error.error.message}</p> <p>トレースID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>アイテム作成</button>;}
ストリームの消費
FastAPIでストリーム応答を設定している場合、useQuery
フックは新しいストリームチャンクが到着するたびにデータを自動更新します。
例:
function MyStreamingComponent() { const api = useMyApi(); const stream = useQuery(api.myStream.queryOptions());
return ( <ul> {(stream.data ?? []).map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
isLoading
とfetchStatus
プロパティを使用してストリームの現在の状態を確認できます。ストリームのライフサイクル:
-
HTTPリクエスト送信
isLoading
:true
fetchStatus
:'fetching'
data
:undefined
-
最初のチャンク受信
isLoading
:false
fetchStatus
:'fetching'
data
: 最初のチャンクを含む配列
-
後続のチャンク受信
isLoading
:false
fetchStatus
:'fetching'
data
: 各チャンク到着時に更新
-
ストリーム完了
isLoading
:false
fetchStatus
:'idle'
data
: 全受信チャンクの配列
クライアント直接使用のストリーミング例
FastAPIでストリーム応答を設定している場合、生成されたクライアントにはfor await
構文を使用したストリームチャンクの非同期反復処理メソッドが含まれます。
例:
function MyStreamingComponent() { const api = useMyApiClient();
const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
生成コードのカスタマイズ
クエリとミューテーション
デフォルトでは、HTTPメソッドPUT
、POST
、PATCH
、DELETE
を使用するFastAPIの操作はミューテーションとみなされ、それ以外はクエリとみなされます。x-query
とx-mutation
を使用してこの動作を変更できます。
x-query
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
生成フックはPOST
メソッド使用時でもqueryOptions
を提供します:
const items = useQuery(api.listItems.queryOptions());
x-mutation
@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
生成フックはGET
メソッド使用時でもmutationOptions
を提供します:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
カスタムページネーションカーソル
デフォルトでは、生成フックはcursor
という名前のパラメータを想定します。x-cursor
拡張でカスタマイズ可能です:
@app.get( "/items", openapi_extra={ "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }
infiniteQueryOptions
の生成を無効化するにはx-cursor
をFalse
に設定:
@app.get( "/items", openapi_extra={ "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }
操作のグループ化
生成されたフックとクライアントメソッドは、FastAPIエンドポイントのOpenAPIタグに基づいて自動的に整理されます。関連する操作を簡単に見つけられるようになります。
例:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...
@app.get( "/users", tags=["users"],)def list(): # ...
生成フックはタグでグループ化されます:
function ItemsAndUsers() { const api = useMyApi();
const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
const users = useQuery(api.users.list.queryOptions());
return ( <div> <h2>アイテム</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>アイテム追加</button>
<h2>ユーザー</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
クライアント直接使用のグループ化例
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { const fetchData = async () => { try { const itemsData = await api.items.list(); setItems(itemsData);
const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('データ取得エラー:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
if (isLoading) { return <div>読み込み中...</div>; }
return ( <div> <h2>アイテム</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<h2>ユーザー</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
エラー
カスタム例外クラス、例外ハンドラ、レスポンスモデルを定義することでエラーレスポンスをカスタマイズできます。生成クライアントはこれらのカスタムエラー型を自動処理します。
カスタムエラーモデルの定義
Pydanticでエラーモデルを定義:
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
カスタム例外の作成
例外クラスを作成:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
例外ハンドラの追加
例外をHTTPレスポンスに変換:
@app.exception_handler(NotFoundException)async def not_found_handler(request: Request, exc: NotFoundException): return JSONResponse( status_code=404, content=exc.message, )
@app.exception_handler(ValidationException)async def validation_error_handler(request: Request, exc: ValidationException): return JSONResponse( status_code=400, content=exc.details.model_dump(), )
レスポンスモデルの指定
エンドポイント定義でエラーステータスコードのレスポンスモデルを指定:
@app.get( "/items/{item_id}", responses={ 404: {"model": str} 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: # ...
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: # ...
Reactでのカスタムエラー型の使用
生成クライアントはカスタムエラー型を処理し、型チェックを可能にします:
function ItemComponent() { const api = useMyApi();
const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { switch (error.status) { case 404: console.error('見つかりません:', error.error); break; case 500: console.error('サーバーエラー:', error.error.message); break; } } });
const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: console.error('検証エラー:', error.error.message); break; case 403: console.error('権限なし:', error.error); break; } } });
if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return <div>{/* コンポーネントコンテンツ */}</div>;}
クライアント直接使用のエラーハンドリング例
function ItemComponent() { const api = useMyApiClient(); const [error, setError] = useState<GetItemError | null>(null);
useEffect(() => { const fetchItem = async () => { try { await api.getItem({ itemId: '123' }); } catch (e) { const err = e as GetItemError; switch (err.status) { case 404: console.error('見つかりません:', err.error); break; case 500: console.error('サーバーエラー:', err.error.message); break; } setError(err); } }; fetchItem(); }, [api]);
if (error?.status === 404) { return <NotFoundMessage message={error.error} />; }
return <div>{/* コンポーネントコンテンツ */}</div>;}
ベストプラクティス
ローディング状態の処理
ユーザーエクスペリエンス向上のため、ローディングとエラー状態を常に処理:
function ItemList() { const api = useMyApi(); const items = useQuery(api.listItems.queryOptions());
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { const err = items.error; switch (err.status) { case 403: return <ErrorMessage message={err.error.reason} />; case 500: return <ErrorMessage message={err.error.message} />; default: return <ErrorMessage message="不明なエラー" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
クライアント直接使用のローディング処理例
function ItemList() { const api = useMyApiClient(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) return <LoadingSpinner />;
if (error) { const err = error as ListItemsError; switch (err.status) { case 403: return <ErrorMessage message={err.error.reason} />; default: return <ErrorMessage message="不明なエラー" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
楽観的更新
ユーザーエクスペリエンス向上のため楽観的更新を実装:
function ItemList() { const queryClient = useQueryClient();
const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { await queryClient.cancelQueries(api.listItems.queryKey()); const previousItems = queryClient.getQueryData(api.listItems.queryKey()); queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) ); return { previousItems }; }, onError: (err, itemId, context) => { queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); }, onSettled: () => { queryClient.invalidateQueries(api.listItems.queryKey()); }, });
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? '削除中...' : '削除'} </button> </li> ))} </ul> );}
クライアント直接使用の楽観的更新例
function ItemList() { const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { const previousItems = items; setItems(items.filter(item => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { setItems(previousItems); } };
return ( <ul> {items.map(item => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>削除</button> </li> ))} </ul> );}
タイプセーフティ
統合は完全なエンドツーエンドのタイプセーフティを提供します。IDEはすべてのAPI呼び出しに対して完全なオートコンプリートと型チェックを提供します:
function ItemForm() { const api = useMyApi();
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { console.log(`作成されたアイテムID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { createItem.mutate(data); };
if (createItem.error) { switch (createItem.error.status) { case 400: return <FormError errors={createItem.error.error.validationErrors} />; case 403: return <AuthError reason={createItem.error.error.reason} />; default: return <ServerError message={createItem.error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> <button type="submit" disabled={createItem.isPending}> {createItem.isPending ? '作成中...' : 'アイテム作成'} </button> </form> );}
クライアント直接使用のタイプセーフティ例
function ItemForm() { const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError; switch (err.status) { case 400: console.error('検証エラー:', err.error.validationErrors); break; case 403: console.error('権限なし:', err.error.reason); break; } setError(err); } };
if (error) { switch (error.status) { case 400: return <FormError errors={error.error.validationErrors} />; case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}
型はFastAPIのOpenAPIスキーマから自動生成されるため、APIへの変更はビルド後にフロントエンドコードに反映されます。