Skip to content

React to Smithy API

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

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

  1. アプリケーションをレンダリングするmain.tsxファイル
  2. 動作するSmithy TypeScript APIバックエンド(ts#apiジェネレーター--framework=smithyで使用して生成)
  3. 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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
変更されるファイルを確認するためにドライランを実行することもできます
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
パラメータデフォルト説明
sourceProject 必須string-ソース プロジェクト
targetProject 必須string-接続先のターゲット プロジェクト
sourceComponent string-接続元のソース コンポーネント (コンポーネント名、ソース プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをソースとして明示的に選択するには '.' を使用します。
targetComponent string-接続先のターゲット コンポーネント (コンポーネント名、ターゲット プロジェクト ルートからの相対パス、またはジェネレーター ID)。プロジェクトをターゲットとして明示的に選択するには '.' を使用します。
preferInstallDependencies booleantrueジェネレーター実行後に依存関係のインストールを優先するかどうか。複数のジェネレーターをバッチ処理する際にインストールを延期する場合はfalseに設定します(後続のジェネレーターがNxプロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。

ジェネレーターは、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を呼び出すことができるバニラAPIクライアントをインスタンス化するためのフックを追加
      • useSigV4.tsx 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フックを介してクライアントを使用することをお勧めしますが、必要に応じてバニラクライアントを使用することもできます。

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

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

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

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

クライアントを直接使用する例はこちらをクリックしてください。

統合には、型付きエラーレスポンスを使用した組み込みのエラーハンドリングが含まれています。<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 is typed as CreateItem400Response
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</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>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</button>;
}
バニラクライアントを直接使用する例はこちらをクリックしてください。

生成されたコードのカスタマイズ

Section titled “生成されたコードのカスタマイズ”

生成されたクライアントをカスタマイズするために使用できるSmithy特性の選択が、ターゲットSmithy modelプロジェクトのextensions.smithyに追加されます。

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

この動作は、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特性を使用してカスタマイズできます。

ページネーショントークンに使用される入力パラメータの名前を変更するには、inputTokenを指定して@cursor特性を適用します:

@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 := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
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 operations are grouped under api.items
const items = useQuery(api.items.listItems.queryOptions());
const createItem = useMutation(api.items.createItem.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.listUsers.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でより良いコード補完が提供されます。

クライアントを直接使用する例はこちらをクリックしてください。

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();
// Query with typed error handling
const getItem = useQuery({
...api.getItem.queryOptions({ itemId: '123' }),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 404:
// error.error is typed as ItemNotFoundError
console.error('Not found:', error.error.message);
break;
case 500:
// error.error is typed as InternalServerError
console.error('Server error:', error.error.message);
console.error('Trace ID:', error.error.traceId);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as InvalidRequestError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedError
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
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>
{/* Component content */}
</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 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}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
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();
// 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>
);
}
バニラクライアントを直接使用する例はこちらをクリックしてください。

統合は、完全なエンドツーエンドの型安全性を提供します。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 InvalidRequestError
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedError
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerError for 500, 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>
);
}
バニラクライアントを直接使用する例はこちらをクリックしてください。

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

Smithy APIがCustom認証(Lambda Authorizer)を使用している場合は、生成されたクライアントプロバイダーを編集して、オーソライザーが期待する認証ヘッダーを追加する必要があります。生成された<ApiName>Provider.tsxfetch設定を探し、リクエストヘッダーにトークンまたはAPIキーを追加してください。