跳转到内容

React 到 FastAPI

connection 生成器提供了一种快速将 React 网站与 FastAPI 后端集成的方法。它为连接到 FastAPI 后端设置了所有必要的配置,以类型安全的方式,包括客户端和 TanStack Query 钩子生成、AWS IAM 和 Cognito 身份验证支持以及适当的错误处理。

在使用此生成器之前,请确保您的 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 项目中的以下文件进行更改:

  • 文件夹scripts
    • generate_open_api.py 添加一个为您的 API 生成 OpenAPI 规范的脚本
  • project.json 在构建中添加一个新目标,该目标调用上述生成脚本

生成器将对 React 应用程序中的以下文件进行更改:

  • 文件夹src
    • 文件夹components
      • <ApiName>Provider.tsx 您的 API 客户端的提供者
      • QueryClientProvider.tsx TanStack React Query 客户端提供者
    • 文件夹hooks
      • use<ApiName>.tsx 添加一个用于调用 API 的钩子,状态由 TanStack Query 管理
      • use<ApiName>Client.tsx 添加一个用于实例化可调用 API 的原生 API 客户端的钩子
      • useSigV4.tsx 添加一个用于使用 SigV4 签名 HTTP 请求的钩子(如果您选择了 IAM 身份验证)
  • project.json 在构建中添加一个新目标,该目标生成类型安全的客户端
  • .gitignore 默认情况下忽略生成的客户端文件

如果尚未存在,生成器还将向您的网站基础设施添加运行时配置,这确保了 FastAPI 的 API URL 在网站中可用,并由 use<ApiName>.tsx 钩子自动配置。

在构建时,将从 FastAPI 的 OpenAPI 规范生成类型安全的客户端。这将向您的 React 应用程序添加三个新文件:

  • 文件夹src
    • 文件夹generated
      • 文件夹<ApiName>
        • types.gen.ts 从 FastAPI 中定义的 pydantic 模型生成的类型
        • client.gen.ts 用于调用 API 的类型安全客户端
        • options-proxy.gen.ts 提供创建 TanStack Query 钩子选项的方法,用于使用 TanStack Query 与 API 交互

生成的类型安全客户端可用于从 React 应用程序调用 FastAPI。建议通过 TanStack Query 钩子使用客户端,但如果您愿意,也可以使用原生客户端。

文件监视器依赖

watch-generate:<ApiName>-client 依赖于 nx watch 命令,该命令需要 Nx Daemon 运行。因此,如果您禁用了守护进程,客户端将不会在对 FastAPI 进行更改时自动重新生成。

生成器提供了一个 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();
// 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)保持其建模类型。传递一个 BlobFile — 例如从 <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 主体,并让 fetch 自动设置 Content-Type(包括多部分边界)。

对于接受 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. 接收流的第一个块

    • isLoading 变为 false
    • fetchStatus 保持 'fetching'
    • data 变为包含第一个块的数组
  3. 接收后续块

    • isLoading 保持 false
    • fetchStatus 保持 'fetching'
    • data 在接收到每个后续块时立即更新
  4. 流完成

    • isLoading 保持 false
    • fetchStatus 变为 'idle'
    • data 是所有接收块的数组
点击此处查看直接使用原生客户端的示例。

默认情况下,FastAPI 中使用 HTTP 方法 PUTPOSTPATCHDELETE 的操作被视为变更,所有其他操作被视为查询。

您可以使用 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());

默认情况下,生成的钩子假定基于游标的分页,参数名为 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-cursor 设置为 False

@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)

生成的客户端将自动处理这些自定义错误类型,允许您对不同的错误响应进行类型检查和处理:

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 授权器),您需要编辑生成的客户端提供者以添加授权器期望的授权标头。在生成的 <ApiName>Provider.tsx 中查找 fetch 配置,并将您的令牌或 API 密钥添加到请求标头中。