Bỏ qua để đến nội dung

React đến Smithy API

Generator connection cung cấp cách thức nhanh chóng để tích hợp website React của bạn với backend Smithy TypeScript API. Nó thiết lập tất cả cấu hình cần thiết để kết nối với Smithy API của bạn theo cách an toàn về kiểu, bao gồm tạo client và hook TanStack Query, hỗ trợ xác thực AWS IAM và Cognito, cùng xử lý lỗi phù hợp.

Trước khi sử dụng generator này, hãy đảm bảo ứng dụng React của bạn có:

  1. File main.tsx render ứng dụng của bạn
  2. Backend Smithy TypeScript API đang hoạt động (được tạo bằng generator ts#api với --framework=smithy)
  3. Cognito Auth được thêm qua generator ts#website#auth nếu kết nối API sử dụng xác thực Cognito hoặc IAM
Ví dụ về cấu trúc main.tsx bắt buộc
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>,
);

Chạy generator này@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
Xây dựng lệnh của bạn5

Bắt buộc

Bắt buộc

Tùy chọn của generator5 tùy chọn
sourceProjectBắt buộcstring

Dự án nguồn

targetProjectBắt buộcstring

Dự án đích để kết nối tới

sourceComponentstring

Component nguồn để kết nối từ đó (tên component, đường dẫn tương đối so với thư mục gốc của dự án nguồn, hoặc id của generator). Sử dụng '.' để chọn rõ ràng dự án làm nguồn.

targetComponentstring

Component đích để kết nối tới (tên component, đường dẫn tương đối so với thư mục gốc của dự án đích, hoặc id của generator). Sử dụng '.' để chọn rõ ràng dự án làm đích.

preferInstallDependenciesbooleanMặc định: true

Có nên cài đặt các dependencies sau khi generator chạy hay không. Đặt thành false để trì hoãn việc cài đặt khi chạy nhiều generator liên tiếp (việc cài đặt vẫn sẽ chạy nếu cần thiết để các generator tiếp theo có thể tính toán Nx project graph); cài đặt một lần vào cuối.

Generator sẽ thực hiện thay đổi các file sau trong ứng dụng React của bạn:

  • Thư mụcsrc
    • Thư mụccomponents
      • <ApiName>Provider.tsx Provider cho API client của bạn
      • QueryClientProvider.tsx TanStack React Query client provider
      • Thư mụcRuntimeConfig/ Component cấu hình runtime cho phát triển cục bộ
    • Thư mụchooks
      • use<ApiName>.tsx Thêm hook để gọi API của bạn với state được quản lý bởi TanStack Query
      • use<ApiName>Client.tsx Thêm hook để khởi tạo vanilla API client có thể gọi API của bạn.
      • useSigV4.tsx Thêm hook để ký các yêu cầu HTTP với SigV4 (nếu bạn chọn xác thực IAM)
  • project.json Một target mới được thêm vào build để tạo type-safe client
  • .gitignore Các file client được tạo ra mặc định bị bỏ qua

Generator cũng sẽ thêm một file vào Smithy model của bạn:

  • Thư mụcmodel
    • Thư mụcsrc
      • extensions.smithy Định nghĩa các trait có thể được sử dụng để tùy chỉnh client được tạo ra

Generator cũng sẽ thêm Runtime Config vào cơ sở hạ tầng website của bạn nếu chưa có, điều này đảm bảo rằng URL API cho Smithy API của bạn có sẵn trong website và được tự động cấu hình bởi hook use<ApiName>.tsx.

Tại thời điểm build, một type-safe client được tạo từ đặc tả OpenAPI của Smithy API. Điều này sẽ thêm ba file mới vào ứng dụng React của bạn:

  • Thư mụcsrc
    • Thư mụcgenerated
      • Thư mục<ApiName>
        • types.gen.ts Các kiểu được tạo từ cấu trúc Smithy model
        • client.gen.ts Type-safe client để gọi API của bạn
        • options-proxy.gen.ts Cung cấp các phương thức để tạo TanStack Query hooks options cho việc tương tác với API của bạn bằng TanStack Query

Type-safe client được tạo ra có thể được sử dụng để gọi Smithy API của bạn từ ứng dụng React. Khuyến nghị sử dụng client thông qua các hook TanStack Query, nhưng bạn có thể sử dụng vanilla client nếu muốn.

Generator cung cấp hook use<ApiName> mà bạn có thể sử dụng để gọi API của mình với TanStack Query.

Bạn có thể sử dụng phương thức queryOptions để lấy các tùy chọn cần thiết để gọi API của bạn bằng hook useQuery của TanStack Query:

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.isPending) return <div>Loading...</div>;
if (item.isError) return <div>Error: {JSON.stringify(item.error.error)}</div>;
return <div>Item: {item.data.name}</div>;
}

Lỗi là một discriminated union của các đối tượng { status, error } thay vì một Error, vì vậy không có message cấp cao nhất để render. Thu hẹp trên status để đọc một response body cụ thể — xem Xử lý lỗi bên dưới.

Nhấp vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

Các hook được tạo ra bao gồm hỗ trợ cho mutations bằng hook useMutation của TanStack Query. Điều này cung cấp cách sạch sẽ để xử lý các thao tác tạo, cập nhật và xóa với trạng thái loading, xử lý lỗi và cập nhật lạc quan.

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: {JSON.stringify(createItem.error.error)}
</div>
)}
</form>
);
}

Bạn cũng có thể thêm callback cho các trạng thái mutation khác nhau:

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({}) });
}
});
Nhấp vào đây để xem ví dụ sử dụng client trực tiếp.

Đối với các endpoint chấp nhận tham số cursor làm đầu vào, các hook được tạo ra cung cấp hỗ trợ cho infinite queries bằng hook useInfiniteQuery của TanStack Query. Điều này giúp dễ dàng triển khai chức năng “load more” hoặc cuộn vô hạn.

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.isPending) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={JSON.stringify(items.error.error)} />;
}
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>
);
}

Các hook được tạo ra tự động xử lý phân trang dựa trên cursor nếu API của bạn hỗ trợ nó. Giá trị nextCursor được trích xuất từ response và được sử dụng để lấy trang tiếp theo.

Nhấp vào đây để xem ví dụ sử dụng client trực tiếp.

Tích hợp bao gồm xử lý lỗi tích hợp sẵn với các response lỗi được định kiểu. Một kiểu <OperationName>Error được tạo ra là một union của một thành viên <OperationName><StatusCode>Error cho mỗi trạng thái lỗi mà operation mô hình hóa. Mỗi thành viên có thuộc tính statuserror, vì vậy việc chuyển đổi trên status sẽ thu hẹp error thành payload của trạng thái đó.

Ví dụ dưới đây giả định CreateItem mô hình hóa các cấu trúc lỗi được định nghĩa ở phần dưới của trang này — một InvalidRequestError 422, một UnauthorizedError 403 và một InternalServerError 500. Chỉ các trạng thái mà model của bạn khai báo mới xuất hiện trong union, vì vậy một case cho trạng thái bạn chưa mô hình hóa là một lỗi biên dịch.

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 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
// error.error is typed as InternalServerErrorResponseContent
return (
<div>
<h2>Server error:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</button>;
}
Nhấp vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

Một số Smithy trait được thêm vào dự án Smithy model đích của bạn trong extensions.smithy mà bạn có thể sử dụng để tùy chỉnh client được tạo ra.

Mặc định, các operation trong Smithy API của bạn sử dụng các phương thức HTTP PUT, POST, PATCHDELETE được coi là mutations, và tất cả các operation khác được coi là queries.

Bạn có thể thay đổi hành vi này bằng cách sử dụng các Smithy trait @query@mutation được thêm vào dự án model của bạn trong extensions.smithy.

Áp dụng trait @query cho Smithy operation của bạn để buộc nó được coi là query:

@http(method: "POST", uri: "/search-items")
@query
operation SearchItems {
input: SearchItemsInput
output: SearchItemsOutput
}

Hook được tạo ra sẽ cung cấp queryOptions ngay cả khi nó sử dụng phương thức HTTP POST:

const items = useQuery(api.searchItems.queryOptions({ term: 'widget' }));

Áp dụng trait @mutation cho Smithy operation của bạn để buộc nó được coi là mutation:

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

Hook được tạo ra sẽ cung cấp mutationOptions ngay cả khi nó sử dụng phương thức HTTP GET:

const startProcessing = useMutation(api.startProcessing.mutationOptions());
startProcessing.mutate({ id: 'some-id' });

Mặc định, các hook được tạo ra giả định phân trang dựa trên cursor với tham số có tên cursor. Bạn có thể tùy chỉnh hành vi này bằng cách sử dụng trait @cursor được thêm vào dự án model của bạn trong extensions.smithy.

Áp dụng trait @cursor với inputToken để thay đổi tên của tham số đầu vào được sử dụng cho token phân trang:

@http(method: "GET", uri: "/paged-items")
@cursor(inputToken: "nextToken")
operation ListPagedItems {
input := {
@httpQuery("nextToken")
nextToken: String
@httpQuery("limit")
limit: Integer
}
output := {
@required
items: ItemList
nextToken: String
}
}

infiniteQueryOptions sau đó phân trang trên nextToken:

const items = useInfiniteQuery({
...api.listPagedItems.infiniteQueryOptions(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextToken || undefined },
),
});

Nếu bạn không muốn tạo infiniteQueryOptions cho một operation có tham số đầu vào có tên cursor, bạn có thể vô hiệu hóa phân trang dựa trên cursor:

@http(method: "GET", uri: "/unpaged-items")
@cursor(enabled: false)
operation ListUnpagedItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
@httpQuery("cursor")
cursor: String
}
output := {
@required
items: ItemList
}
}

api.listUnpagedItems sau đó chỉ cung cấp queryOptions, queryKeyqueryFilter.

Các hook và phương thức client được tạo ra tự động được tổ chức dựa trên @tags trait trong các Smithy operation của bạn. Các operation có cùng tag được nhóm lại với nhau, điều này giúp giữ cho các lời gọi API của bạn được tổ chức và cung cấp hoàn thành mã tốt hơn trong IDE của bạn.

Trong một nhóm, mỗi operation giữ tên camelCased riêng của nó, vì vậy một operation có tên ListItems được gắn thẻ "items" được truy cập tại api.items.listItems — không phải api.items.list.

Ví dụ, với Smithy model này:

service MyService {
operations: [ListItems, CreateItem, ListUsers, CreateUser]
}
@tags(["items"])
@http(method: "GET", uri: "/items")
@readonly
operation ListItems {
input := {}
output := {
@required
items: ItemList
}
}
@tags(["items"])
@http(method: "POST", uri: "/items")
operation CreateItem {
input := {
@required
name: String
}
output := {
@required
id: String
}
}
@tags(["users"])
@http(method: "GET", uri: "/users")
@readonly
operation ListUsers {
input := {}
output := {
@required
users: UserList
}
}
@tags(["users"])
@http(method: "POST", uri: "/users")
operation CreateUser {
input := {
@required
name: String
}
output := {
@required
id: String
}
}

Các hook được tạo ra sẽ được nhóm theo tag:

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?.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2>
<ul>
{users.data?.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

Việc nhóm này giúp dễ dàng tổ chức các lời gọi API của bạn và cung cấp hoàn thành mã tốt hơn trong IDE của bạn.

Nhấp vào đây để xem ví dụ sử dụng client trực tiếp.

Bạn có thể tùy chỉnh các response lỗi trong Smithy API của mình bằng cách định nghĩa các cấu trúc lỗi tùy chỉnh trong Smithy model của bạn. Client được tạo ra sẽ tự động xử lý các kiểu lỗi tùy chỉnh này.

Định nghĩa các cấu trúc lỗi tùy chỉnh

Phần tiêu đề “Định nghĩa các cấu trúc lỗi tùy chỉnh”

Định nghĩa các cấu trúc lỗi của bạn trong Smithy model:

@error("client")
@httpError(422)
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
}

Chỉ định lỗi nào mà các operation của bạn có thể trả về:

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
}

Sử dụng các kiểu lỗi tùy chỉnh trong React

Phần tiêu đề “Sử dụng các kiểu lỗi tùy chỉnh trong React”

Client được tạo ra sẽ tự động xử lý các kiểu lỗi tùy chỉnh này, cho phép bạn kiểm tra kiểu và xử lý các response lỗi khác nhau:

import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() {
const api = useMyApi();
const getItem = useQuery(api.getItem.queryOptions({ itemId: '123' }));
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
switch (getItem.error.status) {
case 404:
// error.error is typed as ItemNotFoundErrorResponseContent
return <NotFoundMessage message={getItem.error.error.message} />;
case 500:
// error.error is typed as InternalServerErrorResponseContent
return <ErrorMessage message={getItem.error.error.message} />;
}
}
return (
<div>
{/* Component content */}
</div>
);
}
Nhấp vào đây để xem ví dụ sử dụng client trực tiếp.

Luôn xử lý trạng thái loading và lỗi để có trải nghiệm người dùng tốt hơn:

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 UnauthorizedErrorResponseContent
return <ErrorMessage message={err.error.reason} />;
case 500:
// err.error is typed as InternalServerErrorResponseContent
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data?.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Nhấp vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

Triển khai cập nhật lạc quan để có trải nghiệm người dùng tốt hơn:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ListItemsResponseContent } from '../generated/my-api/types.gen';
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<ListItemsResponseContent>(
api.listItems.queryKey({}),
);
// Optimistically update to the new value
queryClient.setQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
(old) =>
old && {
...old,
items: old.items.filter((item) => item.id !== itemId),
},
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, _input, 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?.items.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate({ itemId: item.id })}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
);
}
Nhấp vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

Tích hợp cung cấp an toàn về kiểu hoàn toàn từ đầu đến cuối. IDE của bạn sẽ cung cấp tự động hoàn thành đầy đủ và kiểm tra kiểu cho tất cả các lời gọi API của bạn:

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 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerErrorResponseContent 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>
);
}
Nhấp vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

Các kiểu được tự động tạo từ schema OpenAPI của Smithy API, đảm bảo rằng bất kỳ thay đổi nào đối với API của bạn đều được phản ánh trong mã frontend của bạn sau khi build.

Nếu Smithy API của bạn sử dụng xác thực Custom (Lambda Authorizer), bạn sẽ cần chỉnh sửa provider client được tạo ra để thêm các header ủy quyền mà authorizer của bạn mong đợi. Tìm cấu hình fetch trong <ApiName>Provider.tsx được tạo ra và thêm token hoặc API key của bạn vào các header yêu cầu. iName>Provider.tsx` được tạo ra và thêm token hoặc API key của bạn vào các header yêu cầu.