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.
Yêu cầu trước
Phần tiêu đề “Yêu cầu trước”Trước khi sử dụng generator này, đảm bảo ứng dụng React của bạn có:
- File
main.tsxđể render ứng dụng của bạn - Backend FastAPI hoạt động (được tạo bằng FastAPI generator)
- Cognito Auth được thêm qua generator
ts#react-website-authnế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>,);Sử dụng
Phần tiêu đề “Sử dụng”Chạy Generator
Phần tiêu đề “Chạy Generator”- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - api-connection - Fill in the required parameters
- Click
Generate
pnpm nx g @aws/nx-plugin:api-connectionyarn nx g @aws/nx-plugin:api-connectionnpx nx g @aws/nx-plugin:api-connectionbunx nx g @aws/nx-plugin:api-connectionYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:api-connection --dry-runyarn nx g @aws/nx-plugin:api-connection --dry-runnpx nx g @aws/nx-plugin:api-connection --dry-runbunx nx g @aws/nx-plugin:api-connection --dry-runTùy chọn
Phần tiêu đề “Tùy chọn”| Parameter | Type | Default | Description |
|---|---|---|---|
| sourceProject Required | string | - | The source project which will call the API |
| targetProject Required | string | - | The target project containing your API |
Kết quả của Generator
Phần tiêu đề “Kết quả của Generator”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.
Sinh Code
Phần tiêu đề “Sinh Code”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
Sử dụng Code được Sinh
Phần tiêu đề “Sử dụng Code được Sinh”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.
Sử dụng API Hook
Phần tiêu đề “Sử dụng API Hook”Generator cung cấp hook use<ApiName> mà bạn có thể sử dụng để gọi API với TanStack Query.
Queries
Phần tiêu đề “Queries”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>;}Sử dụng API client trực tiếp
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}Mutations
Phần tiêu đề “Mutations”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() }); }});Mutations sử dụng API client trực tiếp
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); // Bạn có thể điều hướng đến item mới // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}Phân trang với Infinite Queries
Phần tiêu đề “Phân trang với Infinite Queries”Đố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.
Phân trang sử dụng API client trực tiếp
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Fetch dữ liệu ban đầu useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Hàm để load thêm items const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}Xử lý Lỗi
Phần tiêu đề “Xử lý Lỗi”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 status và error, 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>;}Xử lý lỗi sử dụng vanilla client trực tiếp
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error được type là CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> <ul> {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>{error.error.reason}</p> </div> ); case 500: case 502: // error.error được type là CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Consuming a Stream
Phần tiêu đề “Consuming a Stream”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 isLoading và fetchStatus để 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:
-
HTTP request để bắt đầu streaming được gửi
isLoadinglàtruefetchStatuslà'fetching'datalàundefined
-
Chunk đầu tiên của stream được nhận
isLoadingtrở thànhfalsefetchStatusvẫn là'fetching'datatrở thành mảng chứa chunk đầu tiên
-
Các chunk tiếp theo được nhận
isLoadingvẫn làfalsefetchStatusvẫn là'fetching'datađược cập nhật với mỗi chunk tiếp theo ngay khi nó được nhận
-
Stream hoàn thành
isLoadingvẫn làfalsefetchStatustrở thành'idle'datalà mảng của tất cả chunks đã nhận
Streaming sử dụng vanilla client trực tiếp
Nếu bạn đã cấu hình FastAPI để stream responses, client được sinh sẽ bao gồm các method type-safe để lặp bất đồng bộ qua các chunks trong stream của bạn bằng cú pháp for await.
Ví dụ:
function MyStreamingComponent() { const api = useMyApiClient();
const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}Tùy chỉnh Code được Sinh
Phần tiêu đề “Tùy chỉnh Code được Sinh”Queries và Mutations
Phần tiêu đề “Queries và Mutations”Mặc định, các operations trong FastAPI của bạn sử dụng HTTP methods PUT, POST, PATCH và DELETE đượ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-query và x-mutation.
x-query
Phần tiêu đề “x-query”@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());x-mutation
Phần tiêu đề “x-mutation”@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 optionsconst startProcessing = useMutation(api.startProcessing.mutationOptions());Custom Pagination Cursor
Phần tiêu đề “Custom Pagination Cursor”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 }Nhóm Operations
Phần tiêu đề “Nhóm Operations”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ụ:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@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.
Grouped operations sử dụng API client trực tiếp
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations được nhóm dưới api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations được nhóm dưới api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Tạo item sử dụng grouped method const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Errors
Phần tiêu đề “Errors”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.
Định nghĩa Custom Error Models
Phần tiêu đề “Định nghĩa Custom Error Models”Đầu tiên, định nghĩa error models của bạn bằng Pydantic:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]Tạo Custom Exceptions
Phần tiêu đề “Tạo Custom Exceptions”Sau đó tạo các custom exception classes cho các error scenarios khác nhau:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = detailsThêm Exception Handlers
Phần tiêu đề “Thêm Exception Handlers”Đăng ký exception handlers để chuyển đổi exceptions của bạn thành HTTP responses:
from fastapi import Requestfrom 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(), )Chỉ định Response Models
Phần tiêu đề “Chỉ định Response Models”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:
@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)Sử dụng Custom Error Types trong React
Phần tiêu đề “Sử dụng Custom Error Types trong React”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> );}Xử lý custom errors với client trực tiếp
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Fetch item với error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error được type dựa trên responses trong FastAPI của bạn const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error là string như chỉ định trong responses console.error('Not found:', err.error); break; case 500: // err.error được type là ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Tạo item với error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error được type là ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error là string như chỉ định trong responses console.error('Forbidden:', err.error); break; } } };
// Component rendering với error handling if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}Best Practices
Phần tiêu đề “Best Practices”Xử lý Loading States
Phần tiêu đề “Xử lý Loading States”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> );}Xử lý loading states sử dụng API client trực tiếp
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; 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.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Optimistic Updates
Phần tiêu đề “Optimistic Updates”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> );}Optimistic updates sử dụng API client trực tiếp
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically xóa item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Khôi phục items trước đó khi lỗi setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}Type Safety
Phần tiêu đề “Type Safety”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> );}Type safety sử dụng API client trực tiếp
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error nếu input không khớp schema await api.createItem(data); } catch (e) { // ✅ Error type bao gồm tất cả possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error được type là CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error được type là CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error được type là CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI có thể sử dụng type narrowing để xử lý các error types khác nhau if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}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.