React to FastAPI
connectionジェネレーターは、ReactウェブサイトとFastAPIバックエンドを素早く統合する方法を提供します。クライアントとTanStack Queryフックの生成、AWS IAMとCognito認証のサポート、適切なエラーハンドリングを含む、FastAPIバックエンドに接続するために必要なすべての設定を型安全な方法でセットアップします。
このジェネレーターを使用する前に、Reactアプリケーションが以下を備えていることを確認してください:
- アプリケーションをレンダリングする
main.tsxファイル - 動作するFastAPIバックエンド(FastAPIジェネレーターを使用して生成)
- CognitoまたはIAM認証を使用するAPIに接続する場合は、
ts#website#authジェネレーター経由で追加されたCognito Auth
必要な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>,);ジェネレーターの実行
Section titled “ジェネレーターの実行”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connection- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - connection - 必須パラメータを入力
- クリック
Generate
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
| sourceProject 必須 | string | - | ソース プロジェクト |
| targetProject 必須 | string | - | 接続先のターゲット プロジェクト |
| sourceComponent | string | - | 接続元のソース コンポーネント (コンポーネント名、ソース プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをソースとして明示的に選択するには '.' を使用します。 |
| targetComponent | string | - | 接続先のターゲット コンポーネント (コンポーネント名、ターゲット プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをターゲットとして明示的に選択するには '.' を使用します。 |
| preferInstallDependencies | boolean | true | ジェネレーター実行後に依存関係のインストールを優先するかどうか。複数のジェネレーターをバッチ処理する際にインストールを延期する場合はfalseに設定します(後続のジェネレーターがNxプロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。 |
ジェネレーターの出力
Section titled “ジェネレーターの出力”ジェネレーターは、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を呼び出すことができるバニラAPIクライアントをインスタンス化するためのフックを追加
- useSigV4.tsx SigV4でHTTPリクエストに署名するためのフックを追加(IAM認証を選択した場合)
- project.json 型安全なクライアントを生成する新しいターゲットがビルドに追加されます
- .gitignore 生成されたクライアントファイルはデフォルトで無視されます
ジェネレーターは、まだ存在しない場合、ウェブサイトインフラストラクチャにランタイム設定も追加します。これにより、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と対話するためのTanStack Queryフックオプションを作成するメソッドを提供
生成されたコードの使用
Section titled “生成されたコードの使用”生成された型安全なクライアントを使用して、ReactアプリケーションからFastAPIを呼び出すことができます。TanStack Queryフック経由でクライアントを使用することをお勧めしますが、必要に応じてバニラクライアントを使用することもできます。
ファイルウォッチャーの依存関係
watch-generate:<ApiName>-clientはnx watchコマンドに依存しており、Nx Daemonが実行されている必要があります。したがって、デーモンを無効にしている場合、FastAPIに変更を加えてもクライアントは自動的に再生成されません。
APIフックの使用
Section titled “APIフックの使用”ジェネレーターは、TanStack QueryでAPIを呼び出すために使用できるuse<ApiName>フックを提供します。
TanStack QueryのuseQueryフックを使用してAPIを呼び出すために必要なオプションを取得するには、queryOptionsメソッドを使用できます:
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>;}ミューテーション
Section titled “ミューテーション”生成されたフックには、TanStack QueryのuseMutationフックを使用したミューテーションのサポートが含まれています。これにより、ローディング状態、エラーハンドリング、楽観的更新を使用して、作成、更新、削除操作を処理するクリーンな方法が提供されます。
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Create a mutation using the generated mutation options const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button>
{createItem.isSuccess && ( <div className="success"> Item created with ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Error: {createItem.error.message} </div> )} </form> );}異なるミューテーション状態のコールバックを追加することもできます:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // This will run when the mutation succeeds console.log('Item created:', data); // You can navigate to the new item navigate(`/items/${data.id}`); }, onError: (error) => { // This will run when the mutation fails console.error('Failed to create item:', error); }, onSettled: () => { // This will run when the mutation completes (success or error) // Good place to invalidate queries that might be affected queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});APIクライアントを直接使用したミューテーション
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); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}ファイルアップロード
Section titled “ファイルアップロード”ファイルアップロードを受け入れるエンドポイントの場合、生成されたクライアントはリクエストをFormDataとして送信します。FastAPIでmultipart/form-dataボディを持つ操作を定義します。例えば、UploadFileを使用します:
from fastapi import UploadFile
@app.post("/files")async def upload_file(file: UploadFile, description: str = "") -> FileMetadata: contents = await file.read() ...バイナリフィールドは生成されたクライアントでBlobとして型付けされ、他のフィールド(上記のdescriptionなど)はモデル化された型を保持します。BlobまたはFileを渡します。例えば、<input type="file">から取得したもの:
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function UploadForm() { const api = useMyApi(); const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { uploadFile.mutate({ file, description: file.name }); } };
return <input type="file" onChange={handleChange} />;}クライアントはFormDataボディを構築し、fetchがContent-Type(マルチパート境界を含む)を自動的に設定できるようにします。
無限クエリによるページネーション
Section titled “無限クエリによるページネーション”入力として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, // Number of items per page }, { // Make sure you define a getNextPageParam function to return // the parameter that should be passed as the 'cursor' for the // next page getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Flatten the pages array to render all items */} <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 ? 'Loading more...' : items.hasNextPage ? 'Load More' : 'No more items'} </button> </div> );}生成されたフックは、APIがサポートしている場合、カーソルベースのページネーションを自動的に処理します。nextCursor値はレスポンスから抽出され、次のページを取得するために使用されます。
APIクライアントを直接使用したページネーション
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);
// Fetch initial data 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]);
// Function to load more items 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 ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}エラーハンドリング
Section titled “エラーハンドリング”統合には、型付きエラーレスポンスを使用した組み込みのエラーハンドリングが含まれています。<operation-name>Error型が生成され、OpenAPI仕様で定義された可能なエラーレスポンスをカプセル化します。各エラーには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: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</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: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> <p>Trace ID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}APIクライアントを直接使用したエラーハンドリング
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: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}ストリームの消費
Section titled “ストリームの消費”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はtruefetchStatusは'fetching'dataはundefined
-
ストリームの最初のチャンクが受信されます
isLoadingはfalseになりますfetchStatusは'fetching'のままdataは最初のチャンクを含む配列になります
-
後続のチャンクが受信されます
isLoadingはfalseのままfetchStatusは'fetching'のままdataは受信されるとすぐに各後続チャンクで更新されます
-
ストリームが完了します
isLoadingはfalseのままfetchStatusは'idle'になりますdataは受信したすべてのチャンクの配列です
APIクライアントを直接使用したストリーミング
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> );}生成されたコードのカスタマイズ
Section titled “生成されたコードのカスタマイズ”クエリとミューテーション
Section titled “クエリとミューテーション”デフォルトでは、HTTPメソッドPUT、POST、PATCH、DELETEを使用するFastAPIの操作はミューテーションと見なされ、その他はすべてクエリと見なされます。
x-queryとx-mutationを使用してこの動作を変更できます。
x-query
Section titled “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...生成されたフックは、POST HTTPメソッドを使用していてもqueryOptionsを提供します:
const items = useQuery(api.listItems.queryOptions());x-mutation
Section titled “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...生成されたフックは、GET HTTPメソッドを使用していてもmutationOptionsを提供します:
// Generated hook will include the custom optionsconst startProcessing = useMutation(api.startProcessing.mutationOptions());カスタムページネーションカーソル
Section titled “カスタムページネーションカーソル”デフォルトでは、生成されたフックはcursorという名前のパラメータを持つカーソルベースのページネーションを想定しています。x-cursor拡張を使用してこの動作をカスタマイズできます:
@app.get( "/items", openapi_extra={ # Specify a different parameter name for the cursor "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={ # Disable cursor-based pagination for this endpoint "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }操作のグループ化
Section titled “操作のグループ化”生成されたフックとクライアントメソッドは、FastAPIエンドポイントのOpenAPIタグに基づいて自動的に整理されます。これにより、API呼び出しを整理しやすくなり、関連する操作を見つけやすくなります。
例:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@app.get( "/users", tags=["users"],)def list(): # ...生成されたフックはこれらのタグによってグループ化されます:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Items operations are grouped under api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users const users = useQuery(api.users.list.queryOptions());
// Usage example const handleCreateItem = () => { createItem.mutate({ name: 'New Item' }); };
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}このグループ化により、API呼び出しを整理しやすくなり、IDEでより良いコード補完が提供されます。
APIクライアントを直接使用したグループ化された操作
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}カスタム例外クラス、例外ハンドラーを定義し、異なるエラーステータスコードのレスポンスモデルを指定することで、FastAPIのエラーレスポンスをカスタマイズできます。生成されたクライアントは、これらのカスタムエラータイプを自動的に処理します。
カスタムエラーモデルの定義
Section titled “カスタムエラーモデルの定義”まず、Pydanticを使用してエラーモデルを定義します:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]カスタム例外の作成
Section titled “カスタム例外の作成”次に、異なるエラーシナリオ用のカスタム例外クラスを作成します:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details例外ハンドラーの追加
Section titled “例外ハンドラーの追加”例外をHTTPレスポンスに変換するために例外ハンドラーを登録します:
from fastapi import Requestfrom fastapi.responses import JSONResponse
@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(), )レスポンスモデルの指定
Section titled “レスポンスモデルの指定”最後に、エンドポイント定義で異なるエラーステータスコードのレスポンスモデルを指定します:
@app.get( "/items/{item_id}", responses={ 404: {"model": str} 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: item = find_item(item_id) if not item: raise NotFoundException(message=f"Item with ID {item_id} not found") return item
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: if not is_valid(item): raise ValidationException( ValidationError( message="Invalid item data", field_errors=["name is required"] ) ) return save_item(item)Reactでのカスタムエラータイプの使用
Section titled “Reactでのカスタムエラータイプの使用”生成されたクライアントは、これらのカスタムエラータイプを自動的に処理し、異なるエラーレスポンスを型チェックして処理できるようにします:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the responses in your FastAPI switch (error.status) { case 404: // error.error is a string as specified in the responses console.error('Not found:', error.error); break; case 500: // error.error is typed as ErrorDetails console.error('Server error:', error.error.message); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as ValidationError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error is a string as specified in the responses console.error('Forbidden:', error.error); break; } } });
// Component rendering with error handling if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}クライアントを直接使用したカスタムエラーの処理
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the responses in your FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is a string as specified in the responses console.error('Not found:', err.error); break; case 500: // err.error is typed as ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error is a string as specified in the responses console.error('Forbidden:', err.error); break; } } };
// Component rendering with error handling if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}ベストプラクティス
Section titled “ベストプラクティス”ローディング状態の処理
Section titled “ローディング状態の処理”より良いユーザーエクスペリエンスのために、常にローディングとエラー状態を処理してください:
import { useQuery } from '@tanstack/react-query';
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: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}APIクライアントを直接使用したローディング状態の処理
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); 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: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}より良いユーザーエクスペリエンスのために楽観的更新を実装してください:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Query to fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, context) => { // If the mutation fails, use the context returned from onMutate to roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Always refetch after error or success to ensure data is in sync with server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Failed to load items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}APIクライアントを直接使用した楽観的更新
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}統合は完全なエンドツーエンドの型安全性を提供します。IDEは、すべてのAPI呼び出しに対して完全な自動補完と型チェックを提供します:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation for creating items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error if onSuccess callback doesn't handle the correct response type onSuccess: (data) => { // data is fully typed based on your API's response schema console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error if input doesn't match schema createItem.mutate(data); };
// Error UI can use type narrowing to handle different error types if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error is typed as CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error is typed as CreateItem5XXResponse for 500, 502, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}APIクライアントを直接使用した型安全性
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error is typed as CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error is typed as CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" 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への変更はビルド後にフロントエンドコードに反映されます。
カスタム認証
Section titled “カスタム認証”FastAPIがCustom認証(Lambda Authorizer)を使用している場合は、生成されたクライアントプロバイダーを編集して、オーソライザーが期待する認証ヘッダーを追加する必要があります。生成された<ApiName>Provider.tsxのfetch設定を探し、リクエストヘッダーにトークンまたはAPIキーを追加してください。