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

React to FastAPI

Generator 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 của bạn theo cách type-safe, bao gồm tạo client và hook TanStack Query, hỗ trợ xác thực AWS IAM và Cognito, và 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 FastAPI hoạt động (được tạo bằng generator FastAPI)
  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 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.

Phụ thuộc file watcher

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();
// 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 callbacks 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ấ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 tải lên file, client được sinh sẽ gửi request dưới dạng FormData. Định nghĩa một operation với body multipart/form-data trong FastAPI của bạn, ví dụ sử dụng UploadFile:

from fastapi import UploadFile
@app.post("/files")
async def upload_file(file: UploadFile, description: str = "") -> FileMetadata:
contents = await file.read()
...

Các trường binary được type là Blob trên client được sinh, và các trường khác (như description ở trên) giữ nguyên các modelled types của chúng. Truyền một Blob hoặc File — ví dụ một file thu được từ <input type="file">:

import { useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function UploadForm() {
const api = useMyApi();
const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadFile.mutate({ file, description: file.name });
}
};
return <input type="file" onChange={handleChange} />;
}

Client xây dựng body FormData và để fetch tự động đặt Content-Type (bao gồm multipart boundary).

Đố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, // 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 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 is typed as 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 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>
<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:

// Generated hook will include the 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={
# Specify a different parameter name for the 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={
# Disable cursor-based pagination for this endpoint
"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 are grouped under api.items
const items = useQuery(api.items.list.queryOptions());
const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.list.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 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 with typed error handling
const getItem = useQuery({
...api.getItem.queryOptions({ itemId: '123' }),
onError: (error) => {
// Error is typed based on the responses in your FastAPI
switch (error.status) {
case 404:
// error.error is a string as specified in the responses
console.error('Not found:', error.error);
break;
case 500:
// error.error is typed as ErrorDetails
console.error('Server error:', error.error.message);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as ValidationError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.field_errors);
break;
case 403:
// error.error is a string as specified in the responses
console.error('Forbidden:', error.error);
break;
}
}
});
// Component rendering with 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 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}
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 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ấ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 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 CreateItem400Response
return (
<FormError
message="Invalid input"
errors={error.error.validationErrors}
/>
);
case 403:
// error.error is typed as CreateItem403Response
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as CreateItem5XXResponse for 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.

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