Skip to content

React から Smithy API へ

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

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

  1. アプリケーションをレンダリングする main.tsx ファイルが存在すること
  2. 動作する Smithy TypeScript API バックエンドが存在すること(ts#smithy-api ジェネレータを使用して生成)
  3. Cognito または IAM 認証を使用する API に接続する場合、ts#react-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 HTMLElement,
);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
  1. インストール Nx Console VSCode Plugin まだインストールしていない場合
  2. VSCodeでNxコンソールを開く
  3. クリック Generate (UI) "Common Nx Commands"セクションで
  4. 検索 @aws/nx-plugin - connection
  5. 必須パラメータを入力
    • クリック Generate
    パラメータ デフォルト 説明
    sourceProject 必須 string - The source project
    targetProject 必須 string - The target project to connect to

    ジェネレータは React アプリケーションの以下のファイルを変更します:

    • Directorysrc
      • Directorycomponents
        • <ApiName>Provider.tsx APIクライアントのプロバイダ
        • QueryClientProvider.tsx TanStack React Query クライアントプロバイダ
        • DirectoryRuntimeConfig/ ローカル開発用のランタイム設定コンポーネント
      • Directoryhooks
        • use<ApiName>.tsx TanStack Query で状態管理されたAPI呼び出し用フック
        • use<ApiName>Client.tsx バニラAPIクライアントインスタンス生成用フック
        • useSigV4.tsx IAM認証でSigV4によるHTTPリクエスト署名用フック(IAM認証選択時)
    • project.json タイプセーフなクライアントを生成する新しいビルドターゲットが追加
    • .gitignore 生成されたクライアントファイルはデフォルトで無視

    ジェネレータは Smithy モデルにもファイルを追加します:

    • Directorymodel
      • Directorysrc
        • extensions.smithy 生成クライアントをカスタマイズするためのトレート定義

    また、ジェネレータはウェブサイトインフラにランタイム設定を追加します(存在しない場合)。これにより、Smithy API の API URL がウェブサイトで利用可能になり、use<ApiName>.tsx フックで自動的に設定されます。

    ビルド時に、Smithy API の OpenAPI 仕様からタイプセーフなクライアントが生成されます。これにより React アプリケーションに3つの新しいファイルが追加されます:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Smithyモデル構造から生成された型
          • client.gen.ts API呼び出し用タイプセーフクライアント
          • options-proxy.gen.ts TanStack Query を使用してAPIと対話するためのTanStack Query フックオプション生成メソッド

    生成されたタイプセーフクライアントはReactアプリからSmithy APIを呼び出すために使用できます。TanStack Queryフック経由での使用が推奨されますが、バニラクライアントも直接使用可能です。

    ジェネレータが提供するuse<ApiName>フックを使用して、TanStack QueryでAPIを呼び出せます。

    queryOptionsメソッドでTanStack QueryのuseQueryフックに必要なオプションを取得できます:

    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, // ページあたりのアイテム数
    }, {
    // 次ページの 'cursor' として渡すパラメータを返す
    // getNextPageParam 関数を必ず定義してください
    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値はレスポンスから抽出され次ページの取得に使用されます。

    クリックでクライアント直接使用例を表示

    統合には型付きエラーレスポンスを持つ組み込みエラーハンドリングが含まれます。<operation-name>Error型が生成され、Smithyモデルで定義された可能性のあるエラーレスポンスをカプセル化します。各エラーには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:
    // error.error は CreateItem400Response として型付け
    return (
    <div>
    <h2>無効な入力:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    case 403:
    // error.error は CreateItem403Response として型付け
    return (
    <div>
    <h2>権限なし:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error は CreateItem5XXResponse として型付け
    return (
    <div>
    <h2>サーバーエラー:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>アイテム作成</button>;
    }
    クリックでバニラクライアント直接使用例を表示

    ターゲットSmithy modelプロジェクトのextensions.smithyに一連のSmithyトレートが追加され、生成クライアントのカスタマイズに使用できます。

    デフォルトで、Smithy APIでHTTPメソッドPUTPOSTPATCHDELETEを使用する操作はミューテーションとして扱われ、他はすべてクエリとして扱われます。

    extensions.smithyのモデルプロジェクトに追加される@query@mutation Smithyトレートを使用してこの動作を変更できます。

    Smithy操作に@queryトレートを適用して、クエリとして扱うよう強制します:

    @http(method: "POST", uri: "/items")
    @query
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }

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

    const items = useQuery(api.listItems.queryOptions());

    Smithy操作に@mutationトレートを適用して、ミューテーションとして扱うよう強制します:

    @http(method: "GET", uri: "/start-processing")
    @mutation
    operation StartProcessing {
    input: StartProcessingInput
    output: StartProcessingOutput
    }

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

    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    カスタムページネーションカーソル

    Section titled “カスタムページネーションカーソル”

    デフォルトで、生成フックはcursorという名前のパラメータを使用したカーソルベースのページネーションを想定します。extensions.smithyのモデルプロジェクトに追加される@cursorトレートを使用してこの動作をカスタマイズできます。

    @cursorトレートにinputTokenを指定して、ページネーショントークンに使用する入力パラメータ名を変更します:

    @http(method: "GET", uri: "/items")
    @cursor(inputToken: "nextToken")
    operation ListItems {
    input := {
    nextToken: String
    limit: Integer
    }
    output := {
    items: ItemList
    nextToken: String
    }
    }

    cursorという名前の入力パラメータを持つ操作に対してinfiniteQueryOptionsを生成したくない場合、カーソルベースのページネーションを無効化できます:

    @cursor(enabled: false)
    operation ListItems {
    input := {
    // 'cursor'という名前の入力パラメータは、デフォルトでこの操作をページネーション操作として扱います
    cursor: String
    }
    output := {
    ...
    }
    }

    生成されたフックとクライアントメソッドは、Smithy操作の@tagsトレートに基づいて自動的に整理されます。同じタグを持つ操作はグループ化され、API呼び出しを整理しやすくし、IDEでのコード補完が向上します。

    例えば、このSmithyモデルの場合:

    service MyService {
    operations: [ListItems, CreateItem, ListUsers, CreateUser]
    }
    @tags(["items"])
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }
    @tags(["items"])
    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    }
    @tags(["users"])
    operation ListUsers {
    input: ListUsersInput
    output: ListUsersOutput
    }
    @tags(["users"])
    operation CreateUser {
    input: CreateUserInput
    output: CreateUserOutput
    }

    生成フックはタグごとにグループ化されます:

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Items操作は api.items 配下にグループ化
    const items = useQuery(api.items.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // Users操作は api.users 配下にグループ化
    const users = useQuery(api.users.listUsers.queryOptions());
    // 使用例
    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}>アイテム追加</button>
    <h2>Users</h2>
    <ul>
    {users.data?.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }

    このグループ化により、API呼び出しを整理しやすくなり、IDEでのコード補完が向上します。

    クリックでクライアント直接使用例を表示

    Smithyモデルでカスタムエラー構造を定義することで、Smithy APIのエラーレスポンスをカスタマイズできます。生成クライアントはこれらのカスタムエラータイプを自動的に処理します。

    Smithyモデルでエラー構造を定義します:

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    fieldErrors: FieldErrorList
    }
    @error("client")
    @httpError(403)
    structure UnauthorizedError {
    @required
    reason: String
    }
    @error("server")
    @httpError(500)
    structure InternalServerError {
    @required
    message: String
    traceId: String
    }
    list FieldErrorList {
    member: FieldError
    }
    structure FieldError {
    @required
    field: String
    @required
    message: String
    }

    操作が返す可能性のあるエラーを指定します:

    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    errors: [
    InvalidRequestError
    UnauthorizedError
    InternalServerError
    ]
    }
    operation GetItem {
    input: GetItemInput
    output: GetItemOutput
    errors: [
    ItemNotFoundError
    InternalServerError
    ]
    }
    @error("client")
    @httpError(404)
    structure ItemNotFoundError {
    @required
    message: String
    }

    Reactでのカスタムエラータイプの使用

    Section titled “Reactでのカスタムエラータイプの使用”

    生成クライアントはこれらのカスタムエラータイプを自動的に処理し、異なるエラーレスポンスの型チェックと処理が可能になります:

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // 型付きエラーハンドリングを伴うクエリ
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // エラーはSmithyモデルのエラーに基づいて型付け
    switch (error.status) {
    case 404:
    // error.error は ItemNotFoundError として型付け
    console.error('見つかりません:', error.error.message);
    break;
    case 500:
    // error.error は InternalServerError として型付け
    console.error('サーバーエラー:', error.error.message);
    console.error('トレースID:', error.error.traceId);
    break;
    }
    }
    });
    // 型付きエラーハンドリングを伴うミューテーション
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error は InvalidRequestError として型付け
    console.error('バリデーションエラー:', error.error.message);
    console.error('フィールドエラー:', error.error.fieldErrors);
    break;
    case 403:
    // error.error は UnauthorizedError として型付け
    console.error('権限なし:', error.error.reason);
    break;
    }
    }
    });
    // エラーハンドリングを伴うコンポーネントレンダリング
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error.message} />;
    } else if (getItem.error.status === 500) {
    return <ErrorMessage message={getItem.error.error.message} />;
    }
    }
    return (
    <div>
    {/* コンポーネントコンテンツ */}
    </div>
    );
    }
    クリックでクライアント直接使用例を表示

    より良いユーザーエクスペリエンスのため、ローディングとエラー状態を常に処理しましょう:

    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 は ListItems403Response として型付け
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error は ListItems5XXResponse として型付け
    return (
    <ErrorMessage
    message={err.error.message}
    />
    );
    default:
    return <ErrorMessage message="不明なエラーが発生しました" />;
    }
    }
    return (
    <ul>
    {items.data.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();
    // アイテムを取得するクエリ
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // 楽観的更新を伴うアイテム削除のミューテーション
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // 進行中の再取得をキャンセル
    await queryClient.cancelQueries({ queryKey: 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) => {
    // ミューテーションが失敗した場合、onMutateから返されたコンテキストを使用してロールバック
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('アイテム削除失敗:', err);
    },
    onSettled: () => {
    // エラーまたは成功後に常に再取得してサーバーとデータを同期
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    },
    });
    if (itemsQuery.isLoading) {
    return <LoadingSpinner />;
    }
    if (itemsQuery.isError) {
    return <ErrorMessage message="アイテムの読み込みに失敗しました" />;
    }
    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呼び出しで完全なオートコンプリートと型チェックを提供します:

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // アイテム作成のためのタイプセーフなミューテーション
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ onSuccessコールバックが正しいレスポンスタイプを処理しない場合は型エラー
    onSuccess: (data) => {
    // data は API のレスポンススキーマに基づいて完全に型付け
    console.log(`作成されたアイテムID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    // ✅ 入力がスキーマと一致しない場合は型エラー
    createItem.mutate(data);
    };
    // エラーUIは型の絞り込みを使用して異なるエラータイプを処理可能
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error は InvalidRequestError として型付け
    return (
    <FormError
    message="無効な入力"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error は UnauthorizedError として型付け
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error は 500 などの場合 InternalServerError として型付け
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: 'New Item' });
    }}>
    {/* フォームフィールド */}
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? '作成中...' : 'アイテム作成'}
    </button>
    </form>
    );
    }
    クリックでバニラクライアント直接使用例を表示

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