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

Kết nối React với Smithy API

Generator api-connection cung cấp cách thức nhanh chóng để tích hợp trang web 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 một cách type-safe, bao gồm việc tạo client và hooks TanStack Query, hỗ trợ xác thực AWS IAM và Cognito cũng như 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. Một file main.tsx để render ứng dụng của bạn
  2. Một backend Smithy TypeScript API đang hoạt động (được tạo bằng generator ts#smithy-api)
  3. Cognito Auth được thêm vào thông qua generator ts#react-website-auth nếu kết nối vớ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>,
);
  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - api-connection
  5. Fill in the required parameters
    • Click Generate
    Parameter Type Default Description
    sourceProject Required string - The source project which will call the API
    targetProject Required string - The target project containing your API

    Generator sẽ thực hiện thay đổi đối vớ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 local
      • 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 HTTP request 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 sẽ bị bỏ qua theo mặc định

    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

    Generator cũng sẽ thêm Runtime Config vào infrastructure của website nếu chưa có, điều này đảm bảo rằng API URL 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 type được tạo từ các 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 options cho TanStack Query hooks để tương tác với API của bạn bằng TanStack Query

    Type-safe client được tạo 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 TanStack Query hooks, 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 bạn với TanStack Query.

    Bạn có thể sử dụng phương thức queryOptions để lấy các options cần thiết cho việc 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ấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Các hook được tạo bao gồm hỗ trợ cho mutations sử dụng hook useMutation của TanStack Query. Điều này cung cấp cách thức rõ ràng để xử lý các thao tác create, update và delete với loading states, xử lý lỗi và optimistic updates.

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    // Tạo mutation sử dụng mutation options được tạo
    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 callbacks cho các mutation states khác nhau:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    // Sẽ chạy khi mutation thành công
    console.log('Item created:', data);
    // Bạn có thể điều hướng đến item mới
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Sẽ chạy khi mutation thất bại
    console.error('Failed to create item:', error);
    },
    onSettled: () => {
    // Sẽ chạy khi mutation hoàn thành (thành công hoặc lỗi)
    // Nơi tốt để invalidate queries có thể bị ảnh hưởng
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Nhấn 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 input, các hook được tạo cung cấp hỗ trợ cho infinite queries sử dụ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 infinite scrolling.

    import { useInfiniteQuery } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemList() {
    const api = useMyApi();
    const items = useInfiniteQuery({
    ...api.listItems.infiniteQueryOptions({
    limit: 10, // Số lượng items mỗi trang
    }, {
    // Đảm bảo bạn định nghĩa hàm getNextPageParam để trả về
    // tham số nên được truyền làm 'cursor' cho
    // trang tiếp theo
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    {/* Làm phẳng mảng pages để render tất cả 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 tự động xử lý phân trang dựa trên cursor nếu API của bạn hỗ trợ. Giá trị nextCursor được trích xuất từ response và được sử dụng để fetch trang tiếp theo.

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

    Tích hợp này bao gồm xử lý lỗi tích hợp sẵn với các typed error responses. Một type <operation-name>Error được tạo để đóng gói các error responses có thể xảy ra được định nghĩa trong Smithy model. Mỗi error 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 error 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 được typed là CreateItem400Response
    return (
    <div>
    <h2>Invalid input:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    case 403:
    // error.error được typed là CreateItem403Response
    return (
    <div>
    <h2>Not authorized:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error được typed là CreateItem5XXResponse
    return (
    <div>
    <h2>Server error:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Create Item</button>;
    }
    Nhấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

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

    Theo mặc định, các operations trong Smithy API của bạn sử dụng các HTTP methods PUT, POST, PATCHDELETE được coi là mutations, và tất cả các operations 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 traits @query@mutation được thêm vào model project của bạn trong extensions.smithy.

    Áp dụng trait @query cho Smithy operation của bạn để buộc nó được xử lý như một query:

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

    Hook được tạo sẽ cung cấp queryOptions mặc dù nó sử dụng HTTP method POST:

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

    Áp dụng trait @mutation cho Smithy operation của bạn để buộc nó được xử lý như một mutation:

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

    Hook được tạo sẽ cung cấp mutationOptions mặc dù nó sử dụng HTTP method GET:

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

    Theo mặc định, các hook được tạo 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 model project của bạn trong extensions.smithy.

    Áp dụng trait @cursor với inputToken để thay đổi tên của tham số input được sử dụng cho pagination token:

    @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ố input 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 := {
    // Tham số input tên 'cursor' sẽ khiến operation này được xử lý như operation phân trang theo mặc định
    cursor: String
    }
    output := {
    ...
    }
    }

    Các hook và client methods được tạo được tự động tổ chức dựa trên @tags trait trong các Smithy operations của bạn. Các operations có cùng tags được nhóm lại với nhau, giúp giữ các API calls của bạn được tổ chức và cung cấp code completion 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 sẽ được nhóm theo tags:

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Các operations Items được nhóm dưới api.items
    const items = useQuery(api.items.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // Các operations Users được nhóm dưới api.users
    const users = useQuery(api.users.listUsers.queryOptions());
    // Ví dụ sử dụng
    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 hơn trong việc tổ chức các API calls của bạn và cung cấp code completion tốt hơn trong IDE của bạn.

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

    Bạn có thể tùy chỉnh error responses trong Smithy API của bạn bằng cách định nghĩa các custom error structures trong Smithy model của bạn. Client được tạo sẽ tự động xử lý các custom error types này.

    Định nghĩa các error structures 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 các errors mà operations 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
    }

    Client được tạo sẽ tự động xử lý các custom error types này, cho phép bạn type-check và xử lý các error responses khác nhau:

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // Query với xử lý lỗi được typed
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // Error được typed dựa trên các errors trong Smithy model của bạn
    switch (error.status) {
    case 404:
    // error.error được typed là ItemNotFoundError
    console.error('Not found:', error.error.message);
    break;
    case 500:
    // error.error được typed là InternalServerError
    console.error('Server error:', error.error.message);
    console.error('Trace ID:', error.error.traceId);
    break;
    }
    }
    });
    // Mutation với xử lý lỗi được typed
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error được typed là InvalidRequestError
    console.error('Validation error:', error.error.message);
    console.error('Field errors:', error.error.fieldErrors);
    break;
    case 403:
    // error.error được typed là UnauthorizedError
    console.error('Unauthorized:', error.error.reason);
    break;
    }
    }
    });
    // Component rendering với xử lý lỗi
    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>
    {/* Nội dung component */}
    </div>
    );
    }
    Nhấn vào đây để xem ví dụ sử dụng client trực tiếp.

    Luôn xử lý loading và error states để 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 được typed là ListItems403Response
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error được typed là 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ấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Triển khai optimistic updates để 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 để fetch items
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // Mutation để xóa items với optimistic updates
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // Hủy bất kỳ refetches đang chờ nào
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Snapshot giá trị trước đó
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Optimistically cập nhật lên giá trị mới
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Trả về context object với snapshot
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // Nếu mutation thất bại, sử dụng context trả về từ onMutate để roll back
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Failed to delete item:', err);
    },
    onSettled: () => {
    // Luôn refetch sau error hoặc success để đảm bảo data đồng bộ với 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ấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Tích hợp này cung cấp type safety end-to-end hoàn chỉnh. IDE của bạn sẽ cung cấp autocompletion và type checking đầy đủ cho tất cả các API calls của bạn:

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // Type-safe mutation để tạo items
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ Type error nếu onSuccess callback không xử lý đúng response type
    onSuccess: (data) => {
    // data được typed đầy đủ dựa trên response schema của API
    console.log(`Item created with ID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    // ✅ Type error nếu input không khớp với schema
    createItem.mutate(data);
    };
    // Error UI có thể sử dụng type narrowing để xử lý các error types khác nhau
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error được typed là InvalidRequestError
    return (
    <FormError
    message="Invalid input"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error được typed là UnauthorizedError
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error được typed là InternalServerError cho 500, v.v.
    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ấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Các types được tự động tạo từ OpenAPI schema 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 frontend code sau khi build.