跳转到内容

React 连接到 Smithy API

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

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

  • 文件夹src
    • 文件夹components
      • <ApiName>Provider.tsx 您的 API 客户端的 Provider
      • QueryClientProvider.tsx TanStack React Query 客户端 provider
      • 文件夹RuntimeConfig/ 用于本地开发的运行时配置组件
    • 文件夹hooks
      • use<ApiName>.tsx 添加一个用于调用 API 的 hook,状态由 TanStack Query 管理
      • use<ApiName>Client.tsx 添加一个用于实例化可调用 API 的原生 API 客户端的 hook。
      • useSigV4.tsx 添加一个用于使用 SigV4 签名 HTTP 请求的 hook(如果您选择了 IAM 身份验证)
  • project.json 添加一个新的构建目标,用于生成类型安全的客户端
  • .gitignore 默认情况下忽略生成的客户端文件

生成器还会向您的 Smithy 模型添加一个文件:

  • 文件夹model
    • 文件夹src
      • extensions.smithy 定义可用于自定义生成的客户端的 traits

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

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

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

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

生成器提供了一个 use<ApiName> hook,您可以使用它通过 TanStack Query 调用 API。

您可以使用 queryOptions 方法检索使用 TanStack Query 的 useQuery hook 调用 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>;
}
点击此处查看直接使用原生客户端的示例。

生成的 hooks 包括对使用 TanStack Query 的 useMutation hook 进行变更的支持。这提供了一种清晰的方式来处理创建、更新和删除操作,包括加载状态、错误处理和乐观更新。

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() });
}
});
点击此处查看直接使用客户端的示例。

对于接受 cursor 参数作为输入的端点,生成的 hooks 提供了使用 TanStack Query 的 useInfiniteQuery hook 进行无限查询的支持。这使得实现”加载更多”或无限滚动功能变得容易。

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 支持,生成的 hooks 会自动处理基于游标的分页。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>;
}
点击此处查看直接使用原生客户端的示例。

extensions.smithy 中,一系列 Smithy traits 被添加到您的目标 Smithy model 项目中,您可以使用它们来自定义生成的客户端。

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

您可以使用添加到 extensions.smithy 中的模型项目的 @query@mutation Smithy traits 来更改此行为。

然后将 @query trait 应用于您的 Smithy 操作,以强制将其视为查询:

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

生成的 hook 将提供 queryOptions,即使它使用 POST HTTP 方法:

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

@mutation trait 应用于您的 Smithy 操作,以强制将其视为变更:

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

生成的 hook 将提供 mutationOptions,即使它使用 GET HTTP 方法:

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

默认情况下,生成的 hooks 假设基于游标的分页,参数名为 cursor。您可以使用添加到 extensions.smithy 中的模型项目的 @cursor trait 来自定义此行为。

@cursor trait 与 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 := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
cursor: String
}
output := {
...
}
}

生成的 hooks 和客户端方法会根据 Smithy 操作中的 @tags trait 自动组织。具有相同标签的操作被分组在一起,这有助于保持 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
}

生成的 hooks 将按标签分组:

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
}

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

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