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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Bạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
Tham sốKiểuMặc địnhMô tả
sourceProject Bắt buộcstring-Dự án nguồn
targetProject Bắt buộcstring-Dự án đích để kết nối tới
sourceComponent string-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 generator id). Sử dụng '.' để chọn rõ ràng dự án làm nguồn.
targetComponent string-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 generator id). Sử dụng '.' để chọn rõ ràng dự án làm đích.
preferInstallDependencies booleantrueCó 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.isLoading) return <div>Loading...</div>;
if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;
}
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: {createItem.error.message}
</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.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>
);
}

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 <operation-name>Error được tạo ra để đóng gói các response lỗi có thể xảy ra được định nghĩa trong Smithy model. Mỗi lỗi có thuộc tính statuserror, và bằng cách kiểm tra giá trị của status, bạn có thể thu hẹp đến một loại lỗi cụ thể.

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>;
}
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.

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

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

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.listItems.queryOptions());

Á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());

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: "/items")
@cursor(inputToken: "nextToken")
operation ListItems {
input := {
nextToken: String
limit: Integer
}
output := {
items: ItemList
nextToken: String
}
}

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:

@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 := {
...
}
}

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.

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

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
}

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?.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>
);
}

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(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
}

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();
// 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>
);
}
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 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>
);
}
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';
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>
);
}
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 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>
);
}
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.