React to FastAPI
Il generatore api-connection
fornisce un modo rapido per integrare il tuo sito React con il backend FastAPI. Configura tutte le impostazioni necessarie per connettersi ai backend FastAPI in modo type-safe, inclusa la generazione di client e hook TanStack Query, supporto per autenticazione AWS IAM e Cognito, e una corretta gestione degli errori.
Prerequisiti
Sezione intitolata “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)
- Autenticazione Cognito aggiunta tramite il generatore
ts#react-website-auth
se ci si connette a 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
Sezione intitolata “Utilizzo”Esegui il Generatore
Sezione intitolata “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
Sezione intitolata “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
Sezione intitolata “Output del Generatore”Il generatore apporterà modifiche ai 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 al build che invoca lo script di generazione sopra
Il generatore apporterà modifiche ai 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 stato gestito da TanStack Query
- use<ApiName>Client.tsx Aggiunge un hook per istanziare il client API 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 al build che genera un client type-safe
- .gitignore I file del client generato vengono ignorati per default
Il generatore aggiungerà anche Runtime Config alla tua infrastruttura del sito web se non è già presente, assicurando che l’URL dell’API per il tuo FastAPI sia disponibile nel sito e configurato automaticamente dall’hook use<ApiName>.tsx
.
Generazione del Codice
Sezione intitolata “Generazione del Codice”Durante il build, viene generato un client type-safe dalla specifica OpenAPI del tuo FastAPI. Questo aggiungerà tre nuovi file alla tua applicazione React:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Tipi generati dai modelli pydantic definiti nel tuo 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 usando TanStack Query
Utilizzo del Codice Generato
Sezione intitolata “Utilizzo del Codice Generato”Il client type-safe generato può essere utilizzato per chiamare il tuo FastAPI dall’applicazione React. È consigliato utilizzare il client tramite gli hook TanStack Query, ma puoi usare il client vanilla se preferisci.
Utilizzo dell’Hook API
Sezione intitolata “Utilizzo dell’Hook API”Il generatore fornisce un hook use<ApiName>
che puoi utilizzare per chiamare la tua API con TanStack Query.
Puoi usare il metodo queryOptions
per recuperare 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
Sezione intitolata “Mutazioni”Gli hook generati includono 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 di mutazione 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) => { // Questo viene eseguito quando la mutazione ha successo console.log('Item created:', data); // Puoi navigare verso il nuovo elemento navigate(`/items/${data.id}`); }, onError: (error) => { // Questo viene eseguito quando la mutazione fallisce console.error('Failed to create item:', error); }, onSettled: () => { // Questo viene eseguito quando la mutazione è completata (successo o errore) // Buon posto per invalidare query che potrebbero essere interessate queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutazioni utilizzando 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 elemento // 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
Sezione intitolata “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 elementi per pagina }, { // Assicurati di definire una funzione getNextPageParam che restituisce // il parametro che dovrebbe essere passato come 'cursor' per la // prossima pagina getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Appiattisci l'array delle pagine per renderizzare tutti gli elementi */} <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 utilizzato per recuperare la pagina successiva.
Paginazione utilizzando 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ù elementi 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
Sezione intitolata “Gestione degli Errori”L’integrazione include una gestione degli 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 utilizzando 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>Input non valido:</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>Non autorizzato:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error è tipizzato come CreateItem5XXResponse return ( <div> <h2>Errore del server:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Crea Elemento</button>;}
Consumo di uno Stream
Sezione intitolata “Consumo di uno Stream”Se hai configurato il tuo FastAPI per streammare le 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 non appena viene ricevuto
-
Lo stream viene completato
isLoading
rimanefalse
fetchStatus
diventa'idle'
data
è un array di tutti i chunk ricevuti
Streaming utilizzando direttamente il client API
Se hai configurato il tuo FastAPI per streammare le risposte, il client generato includerà metodi type-safe per iterare in modo asincrono sui chunk del tuo 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
Sezione intitolata “Personalizzazione del Codice Generato”Query e Mutazioni
Sezione intitolata “Query e Mutazioni”Per default, le operazioni nel tuo FastAPI che usano i metodi HTTP PUT
, POST
, PATCH
e DELETE
sono considerate mutazioni, mentre tutte le altre sono considerate query.
Puoi modificare questo comportamento usando x-query
e x-mutation
.
x-query
Sezione intitolata “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
L’hook generato fornirà queryOptions
nonostante utilizzi il metodo HTTP POST
:
const items = useQuery(api.listItems.queryOptions());
x-mutation
Sezione intitolata “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
L’hook generato fornirà mutationOptions
nonostante utilizzi il metodo HTTP GET
:
// L'hook generato includerà le opzioni personalizzateconst startProcessing = useMutation(api.startProcessing.mutationOptions());
Cursor di Paginazione Personalizzato
Sezione intitolata “Cursor di Paginazione Personalizzato”Per 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 }
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
Sezione intitolata “Raggruppamento Operazioni”Gli hook e i metodi del client generati sono organizzati automaticamente in base ai tag OpenAPI nei tuoi endpoint 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}>Aggiungi Elemento</button>
<h2>Utenti</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 completamento del codice nel tuo IDE.
Operazioni raggruppate utilizzando 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('Errore nel recupero dei dati:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Crea elemento usando il metodo raggruppato const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Errore nella creazione dell\'elemento:', error); } };
if (isLoading) { return <div>Caricamento...</div>; }
return ( <div> <h2>Elementi</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Aggiungi Elemento</button>
<h2>Utenti</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
Puoi personalizzare le risposte di errore nel tuo 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 di Modelli di Errore Personalizzati
Sezione intitolata “Definizione di Modelli di Errore Personalizzati”Prima definisci i tuoi modelli di errore usando Pydantic:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
Creazione di Eccezioni Personalizzate
Sezione intitolata “Creazione di Eccezioni Personalizzate”Poi crea classi di eccezione personalizzate 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 di Gestori di Eccezioni
Sezione intitolata “Aggiunta di Gestori di Eccezioni”Registra i gestori di eccezioni 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 dei Modelli di Risposta
Sezione intitolata “Specifica dei Modelli di Risposta”Infine, specifica i modelli di risposta per diversi codici di stato di errore 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"Elemento con ID {item_id} non trovato") 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="Dati elemento non validi", field_errors=["name è obbligatorio"] ) ) return save_item(item)
Utilizzo di Tipi di Errore Personalizzati in React
Sezione intitolata “Utilizzo di 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 tuo FastAPI switch (error.status) { case 404: // error.error è una stringa come specificato nelle risposte console.error('Non trovato:', error.error); break; case 500: // error.error è tipizzato come ErrorDetails console.error('Errore del server:', 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('Errore di validazione:', error.error.message); console.error('Errori campo:', error.error.field_errors); break; case 403: // error.error è una stringa come specificato nelle risposte console.error('Accesso negato:', error.error); break; } } });
// Renderizzazione 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 elemento 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 tuo FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error è una stringa come specificato nelle risposte console.error('Non trovato:', err.error); break; case 500: // err.error è tipizzato come ErrorDetails console.error('Errore del server:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Crea elemento 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('Errore di validazione:', err.error.message); console.error('Errori campo:', err.error.field_errors); break; case 403: // err.error è una stringa come specificato nelle risposte console.error('Accesso negato:', err.error); break; } } };
// Renderizzazione 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
Sezione intitolata “Best Practices”Gestione degli Stati di Caricamento
Sezione intitolata “Gestione degli Stati di Caricamento”Gestisci sempre gli stati di caricamento e errore 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="Si è verificato un errore sconosciuto" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Gestione stati di caricamento con il client direttamente
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 <div>Caricamento...</div>; }
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="Si è verificato un errore sconosciuto" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Aggiornamenti Ottimistici
Sezione intitolata “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 elementi const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutazione per eliminare elementi 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) );
// Restituisce un oggetto contesto con lo snapshot return { previousItems }; }, onError: (err, itemId, context) => { // Se la mutazione fallisce, usa il contesto restituito da onMutate per ripristinare queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Eliminazione elemento fallita:', err); }, onSettled: () => { // Invalida sempre dopo errore o successo per sincronizzare i dati con il server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Caricamento elementi fallito" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Eliminazione...' : 'Elimina'} </button> </li> ))} </ul> );}
Aggiornamenti ottimistici con il client direttamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Rimozione ottimistica dell'elemento const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Ripristino elementi precedenti in caso di errore setItems(previousItems); console.error('Eliminazione elemento fallita:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Elimina</button> </li> ))} </ul> );}
Type Safety
Sezione intitolata “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 elementi 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 della tua API console.log(`Elemento creato con ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Errore di tipo se l'input non corrisponde allo schema createItem.mutate(data); };
// L'UI degli errori può usare il 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="Input non valido" 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 ? 'Creazione...' : 'Crea Elemento'} </button> </form> );}
Type safety con il client direttamente
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('Errori di validazione:', err.error.validationErrors); break; case 403: // err.error è tipizzato come CreateItem403Response console.error('Accesso negato:', err.error.reason); break; case 500: case 502: // err.error è tipizzato come CreateItem5XXResponse console.error( 'Errore del server:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// L'UI degli errori può usare il type narrowing per gestire diversi tipi di errore if (error) { switch (error.status) { case 400: return ( <FormError message="Input non valido" 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 vengono generati automaticamente dallo schema OpenAPI del tuo FastAPI, assicurando che qualsiasi modifica alla tua API si rifletta nel codice frontend dopo un build.