Skip to content

ReactからFastAPIへ

api-connection ジェネレータは、React ウェブサイトと FastAPI バックエンドを迅速に統合する方法を提供します。型安全な方法で FastAPI バックエンドに接続するために必要なすべての設定(クライアントと TanStack Query フックの生成、AWS IAM および Cognito 認証のサポート、適切なエラーハンドリングなど)をセットアップします。

このジェネレータを使用する前に、React アプリケーションが以下を満たしていることを確認してください:

  1. アプリケーションをレンダリングする main.tsx ファイルが存在すること
  2. 動作する FastAPI バックエンド(FastAPI ジェネレータで生成されたもの)
  3. 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>,
);
  1. インストール Nx Console VSCode Plugin まだインストールしていない場合
  2. VSCodeでNxコンソールを開く
  3. クリック Generate (UI) "Common Nx Commands"セクションで
  4. 検索 @aws/nx-plugin - api-connection
  5. 必須パラメータを入力
    • クリック Generate
    パラメータ デフォルト 説明
    sourceProject 必須 string - The source project which will call the API
    targetProject 必須 string - The target project containing your API

    ジェネレータは 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 と対話するためのフックオプションを作成するメソッドを提供

    生成された型安全なクライアントを使用して、React アプリケーションから FastAPI を呼び出すことができます。TanStack Query フックを介してクライアントを使用することを推奨しますが、バニラクライアントを直接使用することも可能です。

    ジェネレータは 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>;
    }
    バニラクライアントを直接使用する例はこちらをクリック

    生成されたフックは、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() });
    }
    });
    バニラクライアントを直接使用する例はこちらをクリック

    無限クエリによるページネーション

    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 値はレスポンスから抽出され、次のページの取得に使用されます。

    バニラクライアントを直接使用する例はこちらをクリック

    統合には型付きエラーレスポンスを備えた組み込みのエラーハンドリングが含まれます。OpenAPI 仕様で定義された可能なエラーレスポンスをカプセル化する <operation-name>Error 型が生成されます。各エラーは statuserror プロパティを持ち、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>;
    }
    バニラクライアントを直接使用する例はこちらをクリック

    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>
    );
    }

    isLoadingfetchStatus プロパティを使用してストリームの現在の状態を判別可能です。ストリームのライフサイクル:

    1. ストリーム開始の HTTP リクエスト送信

      • isLoading: true
      • fetchStatus: 'fetching'
      • data: undefined
    2. 最初のチャンク受信

      • isLoading: false
      • fetchStatus: 'fetching'
      • data: 最初のチャンクを含む配列
    3. 後続のチャンク受信

      • isLoading: false
      • fetchStatus: 'fetching'
      • data: 受信したチャンクで更新
    4. ストリーム完了

      • isLoading: false
      • fetchStatus: 'idle'
      • data: 全チャンクの配列
    バニラクライアントを直接使用する例はこちらをクリック

    デフォルトでは、HTTP メソッド PUTPOSTPATCHDELETE を使用する FastAPI 操作はミューテーションと見なされ、他はクエリと見なされます。x-queryx-mutation でこの動作を変更できます。

    @app.post(
    "/items",
    openapi_extra={
    "x-query": True
    }
    )
    def list_items():
    # ...

    生成フックは POST メソッドを使用していても queryOptions を提供します:

    const items = useQuery(api.listItems.queryOptions());
    @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-cursorFalse に設定:

    @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 タグに基づいて自動的に整理されます。関連する操作を簡単に見つけられるようになります。

    例:

    items.py
    @app.get(
    "/items",
    tags=["items"],
    )
    def list():
    # ...
    @app.post(
    "/items",
    tags=["items"],
    )
    def create(item: Item):
    # ...
    users.py
    @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>
    );
    }
    バニラクライアントを直接使用する例はこちらをクリック

    カスタム例外クラス、例外ハンドラ、レスポンスモデルを定義することで、FastAPI のエラーレスポンスをカスタマイズできます。生成クライアントはこれらのカスタムエラータイプを自動処理します。

    Pydantic を使用してエラーモデルを定義:

    models.py
    from pydantic import BaseModel
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    異なるエラーシナリオ用の例外クラス:

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    例外を HTTP レスポンスに変換:

    main.py
    @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(),
    )

    エンドポイント定義でエラーステータスコードのレスポンスモデルを指定:

    main.py
    @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 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 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>
    );
    }
    バニラクライアントを直接使用する例はこちらをクリック

    統合は完全なエンドツーエンドの型安全性を提供します。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>
    );
    }
    バニラクライアントを直接使用する例はこちらをクリック

    型は FastAPI の OpenAPI スキーマから自動生成されるため、API への変更はビルド後にフロントエンドコードに反映されます。