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

React đến FastAPI

Generator api-connection cung cấp cách nhanh chóng để tích hợp website React của bạn với backend FastAPI. Nó thiết lập tất cả cấu hình cần thiết để kết nối với các backend FastAPI theo cách type-safe, bao gồm sinh client và hooks TanStack Query, hỗ trợ xác thực AWS IAM và Cognito, cùng với xử lý lỗi phù hợp.

Trước khi sử dụng generator nà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 FastAPI hoạt động (được tạo bằng FastAPI generator)
  3. Cognito Auth được thêm qua generator ts#react-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 cần thiết
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 các file sau trong dự án FastAPI của bạn:

    • Thư mụcscripts
      • generate_open_api.py Thêm script sinh đặc tả OpenAPI cho API của bạn
    • project.json Thêm target mới vào build để gọi script generate ở trên

    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ụ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 Thêm target mới vào build để sinh type-safe client
    • .gitignore Các file client được sinh sẽ được ignore mặc định

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

    Tại thời điểm build, một type-safe client được sinh từ đặc tả OpenAPI của FastAPI. Đ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 sinh từ pydantic models được định nghĩa trong FastAPI của bạn
          • client.gen.ts Type-safe client để gọi API của bạn
          • options-proxy.gen.ts Cung cấp các method để tạo TanStack Query hooks options để tương tác với API của bạn bằng TanStack Query

    Type-safe client được sinh có thể được sử dụng để gọi FastAPI từ ứng dụng React của bạn. 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.

    watch-generate:<ApiName>-client dựa vào lệnh nx watch, yêu cầu Nx Daemon đang chạy. Do đó nếu bạn đã tắt daemon, client sẽ không tự động sinh lại khi thay đổi FastAPI của bạn.

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

    Bạn có thể sử dụng method queryOptions để lấy các options 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ấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Các hooks được sinh 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 sạch sẽ để xử lý các thao tác tạo, cập nhật và xóa 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 sinh
    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 trạng thái mutation 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 hooks được sinh 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>
    {/* Flatten 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 hooks được sinh 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à 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 bao gồm xử lý lỗi tích hợp sẵn với các typed error responses. Type <operation-name>Error được sinh bao gồm các error responses có thể có được định nghĩa trong đặc tả OpenAPI. 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 xuống 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 được type là 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 được type là CreateItem403Response
    return (
    <div>
    <h2>Not authorized:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error được type là 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>;
    }
    Nhấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Nếu bạn đã cấu hình FastAPI để stream responses, hook useQuery của bạn sẽ tự động cập nhật data khi các chunk mới của stream đến.

    Ví dụ:

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

    Bạn có thể sử dụng các thuộc tính isLoadingfetchStatus để xác định trạng thái hiện tại của stream nếu cần. Một stream tuân theo vòng đời này:

    1. HTTP request để bắt đầu streaming được gửi

      • isLoadingtrue
      • fetchStatus'fetching'
      • dataundefined
    2. Chunk đầu tiên của stream được nhận

      • isLoading trở thành false
      • fetchStatus vẫn là 'fetching'
      • data trở thành mảng chứa chunk đầu tiên
    3. Các chunk tiếp theo được nhận

      • isLoading vẫn là false
      • fetchStatus vẫn là 'fetching'
      • data được cập nhật với mỗi chunk tiếp theo ngay khi nó được nhận
    4. Stream hoàn thành

      • isLoading vẫn là false
      • fetchStatus trở thành 'idle'
      • data là mảng của tất cả chunks đã nhận
    Nhấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Mặc định, các operations trong FastAPI của bạn sử dụng HTTP methods PUT, POST, PATCHDELETE được coi là mutations, và tất cả các methods khác được coi là queries.

    Bạn có thể thay đổi hành vi này bằng x-queryx-mutation.

    @app.post(
    "/items",
    openapi_extra={
    "x-query": True
    }
    )
    def list_items():
    # ...

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

    const items = useQuery(api.listItems.queryOptions());
    @app.get(
    "/start-processing",
    openapi_extra={
    "x-mutation": True
    }
    )
    def start_processing():
    # ...

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

    // Hook được sinh sẽ bao gồm custom options
    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Mặc định, các hooks được sinh 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 extension x-cursor:

    @app.get(
    "/items",
    openapi_extra={
    # Chỉ định tên tham số khác cho cursor
    "x-cursor": "page_token"
    }
    )
    def list_items(page_token: str = None, limit: int = 10):
    # ...
    return {
    "items": items,
    "page_token": next_page_token
    }

    Nếu bạn không muốn sinh infiniteQueryOptions cho một operation, bạn có thể đặt x-cursor thành False:

    @app.get(
    "/items",
    openapi_extra={
    # Tắt phân trang dựa trên cursor cho endpoint này
    "x-cursor": False
    }
    )
    def list_items(page: int = 1, limit: int = 10):
    # ...
    return {
    "items": items,
    "total": total_count,
    "page": page,
    "pages": total_pages
    }

    Các hooks và client methods được sinh tự động được tổ chức dựa trên OpenAPI tags trong các endpoints FastAPI của bạn. Điều này giúp giữ các API calls của bạn được tổ chức và giúp dễ dàng tìm các operations liên quan.

    Ví dụ:

    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():
    # ...

    Các hooks được sinh sẽ được nhóm theo các tags này:

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Items operations được nhóm dưới api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Users operations được nhóm dưới api.users
    const users = useQuery(api.users.list.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 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 FastAPI bằng cách định nghĩa các custom exception classes, exception handlers, và chỉ định response models cho các error status codes khác nhau. Client được sinh sẽ tự động xử lý các custom error types này.

    Đầu tiên, định nghĩa error models của bạn bằng Pydantic:

    models.py
    from pydantic import BaseModel
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    Sau đó tạo các custom exception classes cho các error scenarios khác nhau:

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    Đăng ký exception handlers để chuyển đổi exceptions của bạn thành HTTP responses:

    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(),
    )

    Cuối cùng, chỉ định response models cho các error status codes khác nhau trong định nghĩa endpoints của bạn:

    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)

    Client được sinh 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 typed error handling
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // Error được type dựa trên responses trong FastAPI của bạn
    switch (error.status) {
    case 404:
    // error.error là string như chỉ định trong responses
    console.error('Not found:', error.error);
    break;
    case 500:
    // error.error được type là ErrorDetails
    console.error('Server error:', error.error.message);
    break;
    }
    }
    });
    // Mutation với typed error handling
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error được type là ValidationError
    console.error('Validation error:', error.error.message);
    console.error('Field errors:', error.error.field_errors);
    break;
    case 403:
    // error.error là string như chỉ định trong responses
    console.error('Forbidden:', error.error);
    break;
    }
    }
    });
    // Component rendering với 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>
    );
    }
    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 type là ListItems403Response
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error được type là 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>
    );
    }
    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 mọi refetch đang chạy
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Snapshot giá trị trước đó
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Optimistically update sang 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 được trả về từ onMutate để rollback
    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 cung cấp type safety từ đầu đến cuối hoàn chỉnh. IDE của bạn sẽ cung cấp autocompletion và type checking đầy đủ cho tất 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 fully typed 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 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 type là CreateItem400Response
    return (
    <FormError
    message="Invalid input"
    errors={error.error.validationErrors}
    />
    );
    case 403:
    // error.error được type là CreateItem403Response
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error được type là CreateItem5XXResponse cho 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>
    );
    }
    Nhấn vào đây để xem ví dụ sử dụng vanilla client trực tiếp.

    Các types được tự động sinh từ OpenAPI schema của FastAPI, đảm bảo rằng mọi thay đổi đối với API của bạn được phản ánh trong frontend code sau khi build.