React to FastAPI
Il generatore api-connection
fornisce un modo rapido per integrare il tuo sito React con il backend FastAPI. Configura tutto il necessario per connettersi ai backend FastAPI in modo type-safe, includendo la generazione di client e hook per TanStack Query, il supporto per autenticazione AWS IAM e Cognito, e una corretta gestione degli errori.
Prerequisiti
Prima di utilizzare questo generatore, assicurati che la tua applicazione React abbia:
- Un file
main.tsx
che renderizza l’applicazione - Un backend FastAPI funzionante (generato usando il generatore FastAPI)
- Cognito Auth aggiunto tramite il generatore
ts#cloudscape-website-auth
se si connette un’API che utilizza autenticazione Cognito o IAM
Esempio della struttura richiesta per main.tsx
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>,);
Utilizzo
Esegui il Generatore
- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)
nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - api-connection
- Compila i parametri richiesti
- Clicca su
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
Opzioni
Parametro | Tipo | Predefinito | Descrizione |
---|---|---|---|
sourceProject Obbligatorio | string | - | The source project which will call the API |
targetProject Obbligatorio | string | - | The target project containing your API |
Output del Generatore
Il generatore modificherà i seguenti file nel tuo progetto FastAPI:
Directoryscripts
- generate_open_api.py Aggiunge uno script che genera una specifica OpenAPI per la tua API
- project.json Aggiunge un nuovo target alla build che invoca lo script di generazione
Il generatore modificherà i seguenti file nella tua applicazione React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Provider per il client della tua API
- QueryClientProvider.tsx Provider del client TanStack React Query
Directoryhooks
- use<ApiName>.tsx Aggiunge un hook per chiamare la tua API con lo stato gestito da TanStack Query
- use<ApiName>Client.tsx Aggiunge un hook per istanziare il client vanilla che può chiamare la tua API
- useSigV4.tsx Aggiunge un hook per firmare le richieste HTTP con SigV4 (se è stata selezionata l’autenticazione IAM)
- project.json Aggiunge un nuovo target alla build che genera un client type-safe
- .gitignore I file del client generato vengono ignorati di default
Il generatore aggiungerà anche Runtime Config all’infrastruttura del tuo sito web se non già presente, assicurando che l’URL dell’API per il FastAPI sia disponibile nel sito e configurato automaticamente dall’hook use<ApiName>.tsx
.
Generazione del Codice
Durante la build, viene generato un client type-safe dalla specifica OpenAPI del FastAPI. Questo aggiungerà tre nuovi file alla tua applicazione React:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Tipi generati dai modelli pydantic definiti nel FastAPI
- client.gen.ts Client type-safe per chiamare la tua API
- options-proxy.gen.ts Fornisce metodi per creare opzioni di hook TanStack Query per interagire con la tua API
Utilizzo del Codice Generato
Il client type-safe generato può essere utilizzato per chiamare il FastAPI dalla tua applicazione React. Si consiglia di utilizzare il client tramite gli hook TanStack Query, ma è possibile usare il client vanilla se preferisci.
Utilizzo dell’Hook API
Il generatore fornisce un hook use<ApiName>
che puoi utilizzare per chiamare la tua API con TanStack Query.
Query
Puoi usare il metodo queryOptions
per ottenere le opzioni richieste per chiamare la tua API usando l’hook useQuery
di 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>;}
Utilizzo diretto del client API
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>;}
Mutazioni
Gli hook generati includono il supporto per le mutazioni usando l’hook useMutation
di TanStack Query. Questo fornisce un modo pulito per gestire operazioni di creazione, aggiornamento e cancellazione con stati di caricamento, gestione errori e aggiornamenti ottimistici.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Crea una mutazione usando le opzioni generate const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Campi del form */} <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> );}
Puoi anche aggiungere callback per diversi stati della mutazione:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // Eseguito quando la mutazione ha successo console.log('Item created:', data); // Puoi navigare verso il nuovo item navigate(`/items/${data.id}`); }, onError: (error) => { // Eseguito quando la mutazione fallisce console.error('Failed to create item:', error); }, onSettled: () => { // Eseguito al completamento della mutazione (successo o errore) // Buon punto per invalidare query che potrebbero essere affette queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutazioni usando direttamente il client API
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); // Puoi navigare verso il nuovo item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Campi del form */} <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> );}
Paginazione con Infinite Queries
Per endpoint che accettano un parametro cursor
come input, gli hook generati forniscono supporto per infinite queries usando l’hook useInfiniteQuery
di TanStack Query. Questo semplifica l’implementazione di funzionalità “carica più” o scroll infinito.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Numero di item per pagina }, { // Assicurati di definire una funzione getNextPageParam che restituisce // il parametro da passare come 'cursor' per la pagina successiva getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Appiattisci l'array pages per renderizzare tutti gli item */} <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> );}
Gli hook generati gestiscono automaticamente la paginazione basata su cursor se la tua API la supporta. Il valore nextCursor
viene estratto dalla risposta e usato per recuperare la pagina successiva.
Paginazione usando direttamente il client API
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);
// Recupera i dati iniziali 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]);
// Funzione per caricare più item 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> );}
Gestione degli Errori
L’integrazione include una gestione errori integrata con risposte di errore tipizzate. Viene generato un tipo <operation-name>Error
che incapsula le possibili risposte di errore definite nella specifica OpenAPI. Ogni errore ha una proprietà status
e error
, e controllando il valore di status
puoi restringere a un tipo specifico di errore.
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 è tipizzato come 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 è tipizzato come CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error è tipizzato come 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>;}
Gestione errori usando direttamente il client API
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 è tipizzato come 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 è tipizzato come CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error è tipizzato come 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>;}
Consumo di uno Stream
Se hai configurato il FastAPI per streammare risposte, il tuo hook useQuery
aggiornerà automaticamente i dati man mano che arrivano nuovi chunk dello stream.
Ad esempio:
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> );}
Puoi usare le proprietà isLoading
e fetchStatus
per determinare lo stato corrente dello stream se necessario. Uno stream segue questo ciclo di vita:
-
Viene inviata la richiesta HTTP per avviare lo streaming
isLoading
ètrue
fetchStatus
è'fetching'
data
èundefined
-
Viene ricevuto il primo chunk dello stream
isLoading
diventafalse
fetchStatus
rimane'fetching'
data
diventa un array contenente il primo chunk
-
Vengono ricevuti chunk successivi
isLoading
rimanefalse
fetchStatus
rimane'fetching'
data
viene aggiornato con ogni chunk successivo appena ricevuto
-
Lo stream si completa
isLoading
rimanefalse
fetchStatus
diventa'idle'
data
è un array di tutti i chunk ricevuti
Streaming usando direttamente il client API
Se hai configurato il FastAPI per streammare risposte, il client generato includerà metodi type-safe per iterare asincronamente sui chunk dello stream usando la sintassi for await
.
Ad esempio:
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> );}
Personalizzazione del Codice Generato
Query e Mutazioni
Di default, le operazioni nel FastAPI che usano i metodi HTTP PUT
, POST
, PATCH
e DELETE
sono considerate mutazioni, tutte le altre sono considerate query.
Puoi modificare questo comportamento usando x-query
e x-mutation
.
x-query
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
L’hook generato fornirà queryOptions
nonostante usi il metodo HTTP POST
:
const items = useQuery(api.listItems.queryOptions());
x-mutation
@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
L’hook generato fornirà mutationOptions
nonostante usi il metodo HTTP GET
:
// L'hook generato includerà le opzioni personalizzateconst startProcessing = useMutation(api.startProcessing.mutationOptions());
Cursor di Paginazione Personalizzato
Di default, gli hook generati assumono una paginazione basata su cursor con un parametro chiamato cursor
. Puoi personalizzare questo comportamento usando l’estensione x-cursor
:
@app.get( "/items", openapi_extra={ # Specifica un nome diverso per il parametro del cursor "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token # La risposta deve includere il cursor con lo stesso nome }
Se non vuoi generare infiniteQueryOptions
per un’operazione, puoi impostare x-cursor
a False
:
@app.get( "/items", openapi_extra={ # Disabilita la paginazione basata su cursor per questo endpoint "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }
Raggruppamento Operazioni
Gli hook e i metodi del client generati sono organizzati automaticamente in base ai tag OpenAPI negli endpoint del FastAPI. Questo aiuta a mantenere organizzate le chiamate API e facilita la ricerca di operazioni correlate.
Ad esempio:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...
@app.get( "/users", tags=["users"],)def list(): # ...
Gli hook generati saranno raggruppati per questi tag:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Le operazioni items sono raggruppate sotto api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Le operazioni users sono raggruppate sotto api.users const users = useQuery(api.users.list.queryOptions());
// Esempio di utilizzo 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> );}
Questo raggruppamento semplifica l’organizzazione delle chiamate API e fornisce un migliore code completion nel tuo IDE.
Operazioni raggruppate usando direttamente il client API
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);
// Carica i dati useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Operazioni items raggruppate sotto api.items const itemsData = await api.items.list(); setItems(itemsData);
// Operazioni users raggruppate sotto 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 { // Crea item usando il metodo raggruppato 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> );}
Errori
Puoi personalizzare le risposte di errore nel FastAPI definendo classi di eccezioni personalizzate, gestori di eccezioni e specificando modelli di risposta per diversi codici di stato. Il client generato gestirà automaticamente questi tipi di errore personalizzati.
Definizione Modelli di Errore Personalizzati
Prima definisci i modelli di errore usando Pydantic:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
Creazione Eccezioni Personalizzate
Poi crea classi di eccezione per diversi scenari di errore:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
Aggiunta Gestori di Eccezione
Registra i gestori di eccezione per convertire le tue eccezioni in risposte HTTP:
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(), )
Specifica Modelli di Risposta
Infine, specifica i modelli di risposta per diversi codici di stato nelle definizioni degli endpoint:
@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)
Utilizzo Tipi di Errore Personalizzati in React
Il client generato gestirà automaticamente questi tipi di errore personalizzati, permettendoti di fare type-check e gestire diverse risposte di errore:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query con gestione errori tipizzata const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // L'errore è tipizzato in base alle risposte nel FastAPI switch (error.status) { case 404: // error.error è una stringa come specificato nelle risposte console.error('Not found:', error.error); break; case 500: // error.error è tipizzato come ErrorDetails console.error('Server error:', error.error.message); break; } } });
// Mutazione con gestione errori tipizzata const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error è tipizzato come ValidationError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error è una stringa come specificato nelle risposte console.error('Forbidden:', error.error); break; } } });
// Render del componente con gestione errori if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Contenuto del componente */} </div> );}
Gestione errori personalizzati con il client direttamente
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);
// Recupera item con gestione errori useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // L'errore è tipizzato in base alle risposte nel FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error è una stringa come specificato nelle risposte console.error('Not found:', err.error); break; case 500: // err.error è tipizzato come ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Crea item con gestione errori const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error è tipizzato come ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error è una stringa come specificato nelle risposte console.error('Forbidden:', err.error); break; } } };
// Render del componente con gestione errori 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> {/* Contenuto del componente */} </div> );}
Best Practices
Gestione Stati di Caricamento
Gestisci sempre gli stati di caricamento e errori per una migliore esperienza utente:
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 è tipizzato come ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error è tipizzato come 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> );}
Gestione stati di caricamento usando direttamente il client
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 è tipizzato come ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error è tipizzato come 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> );}
Aggiornamenti Ottimistici
Implementa aggiornamenti ottimistici per una migliore esperienza utente:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Query per recuperare gli item const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutazione per eliminare item con aggiornamenti ottimistici const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Annulla eventuali refetch in corso await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot del valore precedente const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Aggiornamento ottimistico al nuovo valore queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Restituisci un oggetto contesto con lo snapshot return { previousItems }; }, onError: (err, itemId, context) => { // Se la mutazione fallisce, usa il contesto per ripristinare queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Rifà sempre il fetch dopo errore o successo per sincronizzazione 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> );}
Aggiornamenti ottimistici usando direttamente il client
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Rimozione ottimistica dell'item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Ripristino item precedenti in caso di errore 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
L’integrazione fornisce type safety end-to-end completa. Il tuo IDE fornirà autocompletamento e type checking per tutte le chiamate API:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Mutazione type-safe per creare item const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Errore di tipo se la callback onSuccess non gestisce il tipo di risposta corretto onSuccess: (data) => { // data è completamente tipizzato in base allo schema di risposta dell'API console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Errore di tipo se l'input non corrisponde allo schema createItem.mutate(data); };
// UI errori può usare type narrowing per gestire diversi tipi di errore if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error è tipizzato come CreateItem400Response return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error è tipizzato come CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error è tipizzato come CreateItem5XXResponse per 500, 502, ecc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Campi del form */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}
Type safety usando direttamente il client
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Errore di tipo se l'input non corrisponde allo schema await api.createItem(data); } catch (e) { // ✅ Il tipo errore include tutte le possibili risposte di errore const err = e as CreateItemError; switch (err.status) { case 400: // err.error è tipizzato come CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error è tipizzato come CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error è tipizzato come CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// UI errori può usare type narrowing per gestire diversi tipi di errore 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>;}
I tipi sono generati automaticamente dallo schema OpenAPI del FastAPI, assicurando che qualsiasi modifica all’API si rifletta nel codice frontend dopo una build.