React から Smithy API へ
api-connection
ジェネレータは、React ウェブサイトと Smithy TypeScript API バックエンドを迅速に統合する方法を提供します。タイプセーフな方法で Smithy API に接続するために必要なすべての設定(クライアントと TanStack Query フックの生成、AWS IAM および Cognito 認証のサポート、適切なエラーハンドリングなど)を行います。
このジェネレータを使用する前に、React アプリケーションが以下を満たしていることを確認してください:
- アプリケーションをレンダリングする
main.tsx
ファイルが存在すること - 動作する Smithy TypeScript API バックエンドが存在すること(
ts#smithy-api
ジェネレータを使用して生成) - 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>,);
ジェネレータの実行
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 “ジェネレータの出力”ジェネレータは 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 フックオプション生成メソッド
生成コードの使用方法
Section titled “生成コードの使用方法”生成されたタイプセーフクライアントはReactアプリからSmithy APIを呼び出すために使用できます。TanStack Queryフック経由での使用が推奨されますが、バニラクライアントも直接使用可能です。
APIフックの使用
Section titled “APIフックの使用”ジェネレータが提供する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>;}
バニラクライアントの直接使用
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> );}
生成フックはカーソルベースのページネーションを自動処理します。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 “エラーハンドリング”統合には型付きエラーレスポンスが含まれます。<operation-name>Error
型でSmithyモデルで定義された可能性のあるエラーをカプセル化します。
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> </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> </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> </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> </div> ); } }
return <button onClick={handleClick}>アイテム作成</button>;}
生成コードのカスタマイズ
Section titled “生成コードのカスタマイズ”Smithyモデルのextensions.smithy
に追加されるトレートを使用して生成クライアントをカスタマイズできます。
クエリとミューテーション
Section titled “クエリとミューテーション”デフォルトでPUT
、POST
、PATCH
、DELETE
メソッドはミューテーション、他はクエリとして扱われます。@query
と@mutation
トレートでこの動作を変更できます。
@query
Section titled “@query”POST
メソッドでもクエリとして扱いたい場合:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}
@mutation
Section titled “@mutation”GET
メソッドでもミューテーションとして扱いたい場合:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}
カスタムページネーションカーソル
Section titled “カスタムページネーションカーソル”デフォルトのcursor
パラメータ名を変更する場合:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}
ページネーションを無効化する場合:
@cursor(enabled: false)operation ListItems { input := { cursor: String } output := { ... }}
操作のグループ化
Section titled “操作のグループ化”@tags
トレートを使用して操作をグループ化できます。同じタグの操作はグループ化され、IDEでのコード補完が向上します。
@tags(["items"])operation ListItems { ... }
@tags(["items"])operation CreateItem { ... }
@tags(["users"])operation ListUsers { ... }
生成フックはタグごとにグループ化されます:
const items = useQuery(api.items.listItems.queryOptions());const createItem = useMutation(api.items.createItem.mutationOptions());const users = useQuery(api.users.listUsers.queryOptions());
バニラクライアントでのグループ化操作
const itemsData = await api.items.listItems();const usersData = await api.users.listUsers();
Smithyモデルでカスタムエラー構造を定義できます。生成クライアントはこれらのエラータイプを自動処理します。
カスタムエラー構造の定義
Section titled “カスタムエラー構造の定義”@error("client")@httpError(400)structure InvalidRequestError { @required message: String fieldErrors: FieldErrorList}
操作へのエラー追加
Section titled “操作へのエラー追加”operation CreateItem { ... errors: [ InvalidRequestError UnauthorizedError ]}
Reactでのカスタムエラータイプの使用
Section titled “Reactでのカスタムエラータイプの使用”switch (error.status) { case 400: console.error('バリデーションエラー:', error.error.fieldErrors); break; case 403: console.error('権限不足:', error.error.reason); break;}
バニラクライアントでのカスタムエラーハンドリング
try { await api.createItem(data);} catch (e) { const err = e as CreateItemError; switch (err.status) { case 400: console.error('バリデーションエラー:', err.error.fieldErrors); break; case 403: console.error('権限不足:', err.error.reason); break; }}
ベストプラクティス
Section titled “ベストプラクティス”ローディング状態の処理
Section titled “ローディング状態の処理”ユーザーエクスペリエンス向上のため、ローディングとエラー状態を常に処理しましょう:
if (items.isLoading) { return <LoadingSpinner />;}
if (items.isError) { return <ErrorMessage message={items.error.message} />;}
バニラクライアントでのローディング状態処理
if (loading) { return <div>読み込み中...</div>;}
if (error) { return <ErrorMessage message={error.message} />;}
ユーザーエクスペリエンス向上のため、楽観的更新を実装しましょう:
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); }});
バニラクライアントでの楽観的更新
const handleDelete = async (itemId) => { const previousItems = items; setItems(items.filter(item => item.id !== itemId)); try { await api.deleteItem(itemId); } catch (error) { setItems(previousItems); }};
タイプセーフティ
Section titled “タイプセーフティ”統合は完全なエンドツーエンドのタイプセーフティを提供します。IDEはすべてのAPI呼び出しで完全なオートコンプリートと型チェックを提供します。
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { console.log(`作成されたアイテムID: ${data.id}`); },});
const handleSubmit = (data: CreateItemInput) => { createItem.mutate(data); // 入力スキーマの型チェック};
バニラクライアントでのタイプセーフティ
const handleSubmit = async (data: CreateItemInput) => { try { await api.createItem(data); // 入力スキーマの型チェック } catch (e) { const err = e as CreateItemError; // エラータイプの型チェック }};
生成された型はSmithy APIのOpenAPIスキーマから自動生成されるため、API変更後はビルドすることでフロントエンドコードに反映されます。