Skip to content

React to FastAPI

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

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

  1. アプリケーションをレンダリングするmain.tsxファイル
  2. 動作するFastAPIバックエンド(FastAPIジェネレーターを使用して生成)
  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プロジェクトグラフを計算できるよう、必要に応じてインストールは実行されます)。最後に一度だけインストールします。

ジェネレーターは、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を呼び出すことができるバニラAPIクライアントをインスタンス化するためのフックを追加
      • useSigV4.tsx SigV4でHTTPリクエストに署名するためのフックを追加(IAM認証を選択した場合)
  • 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と対話するためのTanStack Queryフックオプションを作成するメソッドを提供

生成された型安全なクライアントを使用して、ReactアプリケーションからFastAPIを呼び出すことができます。TanStack Queryフック経由でクライアントを使用することをお勧めしますが、必要に応じてバニラクライアントを使用することもできます。

ファイルウォッチャーの依存関係

watch-generate:<ApiName>-clientnx watchコマンドに依存しており、Nx Daemonが実行されている必要があります。したがって、デーモンを無効にしている場合、FastAPIに変更を加えてもクライアントは自動的に再生成されません。

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

ファイルアップロードを受け入れるエンドポイントの場合、生成されたクライアントはリクエストをFormDataとして送信します。FastAPIでmultipart/form-dataボディを持つ操作を定義します。例えば、UploadFileを使用します:

from fastapi import UploadFile
@app.post("/files")
async def upload_file(file: UploadFile, description: str = "") -> FileMetadata:
contents = await file.read()
...

バイナリフィールドは生成されたクライアントでBlobとして型付けされ、他のフィールド(上記のdescriptionなど)はモデル化された型を保持します。BlobまたはFileを渡します。例えば、<input type="file">から取得したもの:

import { useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function UploadForm() {
const api = useMyApi();
const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadFile.mutate({ file, description: file.name });
}
};
return <input type="file" onChange={handleChange} />;
}

クライアントはFormDataボディを構築し、fetchContent-Type(マルチパート境界を含む)を自動的に設定できるようにします。

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

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型が生成され、OpenAPI仕様で定義された可能なエラーレスポンスをカプセル化します。各エラーには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>
<ul>
{createItem.error.error.validationErrors.map((err) => (
<li key={err.field}>{err.message}</li>
))}
</ul>
</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>
<p>Trace ID: {createItem.error.error.traceId}</p>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</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リクエストが送信されます

    • isLoadingtrue
    • fetchStatus'fetching'
    • dataundefined
  2. ストリームの最初のチャンクが受信されます

    • isLoadingfalseになります
    • fetchStatus'fetching'のまま
    • dataは最初のチャンクを含む配列になります
  3. 後続のチャンクが受信されます

    • isLoadingfalseのまま
    • fetchStatus'fetching'のまま
    • dataは受信されるとすぐに各後続チャンクで更新されます
  4. ストリームが完了します

    • isLoadingfalseのまま
    • fetchStatus'idle'になります
    • dataは受信したすべてのチャンクの配列です
バニラクライアントを直接使用する例はこちらをクリックしてください。

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

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

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

x-queryx-mutationを使用してこの動作を変更できます。

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

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

const items = useQuery(api.listItems.queryOptions());
@app.get(
"/start-processing",
openapi_extra={
"x-mutation": True
}
)
def start_processing():
# ...

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

// Generated hook will include the custom options
const startProcessing = useMutation(api.startProcessing.mutationOptions());

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

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

デフォルトでは、生成されたフックはcursorという名前のパラメータを持つカーソルベースのページネーションを想定しています。x-cursor拡張を使用してこの動作をカスタマイズできます:

@app.get(
"/items",
openapi_extra={
# Specify a different parameter name for the cursor
"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={
# Disable cursor-based pagination for this endpoint
"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タグに基づいて自動的に整理されます。これにより、API呼び出しを整理しやすくなり、関連する操作を見つけやすくなります。

例:

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():
# ...

生成されたフックはこれらのタグによってグループ化されます:

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.list.queryOptions());
const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.list.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でより良いコード補完が提供されます。

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

カスタム例外クラス、例外ハンドラーを定義し、異なるエラーステータスコードのレスポンスモデルを指定することで、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
from fastapi import Request
from fastapi.responses import JSONResponse
@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:
item = find_item(item_id)
if not item:
raise NotFoundException(message=f"Item with ID {item_id} not found")
return item
@app.post(
"/items",
responses={
400: {"model": ValidationError},
403: {"model": str}
}
)
def create_item(item: Item) -> Item:
if not is_valid(item):
raise ValidationException(
ValidationError(
message="Invalid item data",
field_errors=["name is required"]
)
)
return save_item(item)

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 responses in your FastAPI
switch (error.status) {
case 404:
// error.error is a string as specified in the responses
console.error('Not found:', error.error);
break;
case 500:
// error.error is typed as ErrorDetails
console.error('Server error:', error.error.message);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as ValidationError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.field_errors);
break;
case 403:
// error.error is a string as specified in the responses
console.error('Forbidden:', error.error);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
if (getItem.error.status === 404) {
return <NotFoundMessage message={getItem.error.error} />;
} else {
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}
details={`Trace ID: ${err.error.traceId}`}
/>
);
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 CreateItem400Response
return (
<FormError
message="Invalid input"
errors={error.error.validationErrors}
/>
);
case 403:
// error.error is typed as CreateItem403Response
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as CreateItem5XXResponse for 500, 502, 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>
);
}
バニラクライアントを直接使用する例はこちらをクリックしてください。

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

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