ReactからFastAPIへ
api-connection
ジェネレータは、React ウェブサイトと FastAPI バックエンドを迅速に統合する方法を提供します。型安全な方法で FastAPI バックエンドに接続するために必要なすべての設定(クライアントと TanStack Query フックの生成、AWS IAM および Cognito 認証のサポート、適切なエラーハンドリングなど)をセットアップします。
このジェネレータを使用する前に、React アプリケーションが以下を満たしていることを確認してください:
- アプリケーションをレンダリングする
main.tsx
ファイルが存在すること - 動作する FastAPI バックエンド(FastAPI ジェネレータで生成されたもの)
- Cognito または IAM 認証を使用する API に接続する場合、
ts#cloudscape-website-auth
ジェネレータ で Cognito 認証が追加されていること
必要な 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 HTLElement,);root.render( <StrictMode> <App /> </StrictMode>,);
ジェネレータの実行
Section titled “ジェネレータの実行”- インストール 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 |
ジェネレータの出力
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 クライアントをインスタンス化するフックを追加
- useSigV4.tsx IAM 認証を選択した場合、SigV4 で HTTP リクエストに署名するフックを追加
- 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 と対話するためのフックオプションを作成するメソッドを提供
生成コードの使用
Section titled “生成コードの使用”生成された型安全なクライアントを使用して、React アプリケーションから FastAPI を呼び出すことができます。TanStack Query フックを介してクライアントを使用することを推奨しますが、バニラクライアントを直接使用することも可能です。
API フックの使用
Section titled “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>;}
バニラクライアントの直接使用
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(); // 生成されたミューテーションオプションを使用 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> );}
無限クエリによるページネーション
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, // ページあたりのアイテム数 }, { 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> );}
エラーハンドリング
Section titled “エラーハンドリング”統合には型付きエラーレスポンスを備えた組み込みのエラーハンドリングが含まれます。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>;}
ストリームの消費
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
: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> );}
生成コードのカスタマイズ
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
メソッドを使用していても 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
メソッドを使用していても mutationOptions
を提供します:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
カスタムページネーションカーソル
Section titled “カスタムページネーションカーソル”デフォルトでは、生成フックは 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 }
操作のグループ化
Section titled “操作のグループ化”生成フックとクライアントメソッドは、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> );}
カスタム例外クラス、例外ハンドラ、レスポンスモデルを定義することで、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 レスポンスに変換:
@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: # ...
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: # ...
React でのカスタムエラータイプの使用
Section titled “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; setError(err); switch (err.status) { case 404: console.error('見つかりません:', err.error); break; case 500: console.error('サーバーエラー:', err.error.message); break; } } };
fetchItem(); }, [api]);
if (error?.status === 404) { return <NotFoundMessage message={error.error} />; }
return <div>{/* コンテンツ */}</div>;}
ベストプラクティス
Section titled “ベストプラクティス”ローディング状態の処理
Section titled “ローディング状態の処理”ユーザーエクスペリエンス向上のため、ローディングとエラー状態を常に処理してください:
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) 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) { const error = createItem.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={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> <button type="submit">アイテム作成</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; 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 への変更はビルド後にフロントエンドコードに反映されます。