React vers FastAPI
Le générateur connection fournit un moyen d’intégrer rapidement votre site web React avec votre backend FastAPI. Il configure toute la configuration nécessaire pour se connecter à vos backends FastAPI de manière type-safe, y compris la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito et une gestion appropriée des erreurs.
Prérequis
Section intitulée « Prérequis »Avant d’utiliser ce générateur, assurez-vous que votre application React dispose de :
- Un fichier
main.tsxqui rend votre application - Un backend FastAPI fonctionnel (généré à l’aide du générateur FastAPI)
- Cognito Auth ajouté via le générateur
ts#website#authsi vous vous connectez à une API qui utilise l’authentification Cognito ou IAM
Exemple de structure main.tsx requise
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>,);Utilisation
Section intitulée « Utilisation »Exécuter le générateur
Section intitulée « Exécuter le générateur »pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:connection --dry-runyarn nx g @aws/nx-plugin:connection --dry-runnpx nx g @aws/nx-plugin:connection --dry-runbunx nx g @aws/nx-plugin:connection --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- Cliquez sur
Generate
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| sourceProject Requis | string | - | Le projet source |
| targetProject Requis | string | - | Le projet cible auquel se connecter |
| sourceComponent | string | - | Le composant source depuis lequel se connecter (nom du composant, chemin relatif à la racine du projet source, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme source. |
| targetComponent | string | - | Le composant cible auquel se connecter (nom du composant, chemin relatif à la racine du projet cible, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme cible. |
| preferInstallDependencies | boolean | true | Indique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin. |
Sortie du générateur
Section intitulée « Sortie du générateur »Le générateur apportera des modifications aux fichiers suivants dans votre projet FastAPI :
Répertoirescripts
- generate_open_api.py Ajoute un script qui génère une spécification OpenAPI pour votre API
- project.json Une nouvelle cible est ajoutée au build qui invoque le script de génération ci-dessus
Le générateur apportera des modifications aux fichiers suivants dans votre application React :
Répertoiresrc
Répertoirecomponents
- <ApiName>Provider.tsx Provider pour votre client API
- QueryClientProvider.tsx Provider du client TanStack React Query
Répertoirehooks
- use<ApiName>.tsx Ajoute un hook pour appeler votre API avec l’état géré par TanStack Query
- use<ApiName>Client.tsx Ajoute un hook pour instancier le client API vanilla qui peut appeler votre API.
- useSigV4.tsx Ajoute un hook pour signer les requêtes HTTP avec SigV4 (si vous avez sélectionné l’authentification IAM)
- project.json Une nouvelle cible est ajoutée au build qui génère un client type-safe
- .gitignore Les fichiers client générés sont ignorés par défaut
Le générateur ajoutera également Runtime Config à votre infrastructure de site web si elle n’est pas déjà présente, ce qui garantit que l’URL de l’API pour votre FastAPI est disponible dans le site web et automatiquement configurée par le hook use<ApiName>.tsx.
Génération de code
Section intitulée « Génération de code »Au moment du build, un client type-safe est généré à partir de la spécification OpenAPI de votre FastAPI. Cela ajoutera trois nouveaux fichiers à votre application React :
Répertoiresrc
Répertoiregenerated
Répertoire<ApiName>
- types.gen.ts Types générés à partir des modèles pydantic définis dans votre FastAPI
- client.gen.ts Client type-safe pour appeler votre API
- options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query pour interagir avec votre API en utilisant TanStack Query
Utilisation du code généré
Section intitulée « Utilisation du code généré »Le client type-safe généré peut être utilisé pour appeler votre FastAPI depuis votre application React. Il est recommandé d’utiliser le client via les hooks TanStack Query, mais vous pouvez utiliser le client vanilla si vous préférez.
Dépendance du File Watcher
watch-generate:<ApiName>-client s’appuie sur la commande nx watch, qui nécessite que le Nx Daemon soit en cours d’exécution. Par conséquent, si vous avez désactivé le daemon, le client ne se régénérera pas automatiquement lors de modifications de votre FastAPI.
Utilisation du hook API
Section intitulée « Utilisation du hook API »Le générateur fournit un hook use<ApiName> que vous pouvez utiliser pour appeler votre API avec TanStack Query.
Requêtes
Section intitulée « Requêtes »Vous pouvez utiliser la méthode queryOptions pour récupérer les options requises pour appeler votre API en utilisant le hook useQuery de 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>;}Utilisation directe du 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>;}Mutations
Section intitulée « Mutations »Les hooks générés incluent la prise en charge des mutations à l’aide du hook useMutation de TanStack Query. Cela fournit un moyen propre de gérer les opérations de création, de mise à jour et de suppression avec des états de chargement, une gestion des erreurs et des mises à jour optimistes.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Create a mutation using the generated mutation options const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button>
{createItem.isSuccess && ( <div className="success"> Item created with ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Error: {createItem.error.message} </div> )} </form> );}Vous pouvez également ajouter des callbacks pour différents états de mutation :
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // This will run when the mutation succeeds console.log('Item created:', data); // You can navigate to the new item navigate(`/items/${data.id}`); }, onError: (error) => { // This will run when the mutation fails console.error('Failed to create item:', error); }, onSettled: () => { // This will run when the mutation completes (success or error) // Good place to invalidate queries that might be affected queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});Mutations utilisant directement le 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); // You can navigate to the new item // 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> );}Téléchargement de fichiers
Section intitulée « Téléchargement de fichiers »Pour les endpoints qui acceptent un téléchargement de fichier, le client généré envoie la requête en tant que FormData. Définissez une opération avec un corps multipart/form-data dans votre FastAPI, par exemple en utilisant UploadFile :
from fastapi import UploadFile
@app.post("/files")async def upload_file(file: UploadFile, description: str = "") -> FileMetadata: contents = await file.read() ...Les champs binaires sont typés comme Blob sur le client généré, et les autres champs (comme description ci-dessus) conservent leurs types modélisés. Passez un Blob ou File — par exemple un obtenu à partir d’un <input type="file"> :
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function UploadForm() { const api = useMyApi(); const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { uploadFile.mutate({ file, description: file.name }); } };
return <input type="file" onChange={handleChange} />;}Le client construit un corps FormData et laisse fetch définir automatiquement le Content-Type (y compris la limite multipart).
Pagination avec requêtes infinies
Section intitulée « Pagination avec requêtes infinies »Pour les endpoints qui acceptent un paramètre cursor en entrée, les hooks générés fournissent la prise en charge des requêtes infinies à l’aide du hook useInfiniteQuery de TanStack Query. Cela facilite l’implémentation de la fonctionnalité “charger plus” ou de défilement infini.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Number of items per page }, { // Make sure you define a getNextPageParam function to return // the parameter that should be passed as the 'cursor' for the // next page getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Flatten the pages array to render all items */} <ul> {items.data.pages.flatMap(page => page.items.map(item => ( <li key={item.id}>{item.name}</li> )) )} </ul>
<button onClick={() => items.fetchNextPage()} disabled={!items.hasNextPage || items.isFetchingNextPage} > {items.isFetchingNextPage ? 'Loading more...' : items.hasNextPage ? 'Load More' : 'No more items'} </button> </div> );}Les hooks générés gèrent automatiquement la pagination basée sur le curseur si votre API la prend en charge. La valeur nextCursor est extraite de la réponse et utilisée pour récupérer la page suivante.
Pagination utilisant directement le 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);
// Fetch initial data 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]);
// Function to load more 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> );}Gestion des erreurs
Section intitulée « Gestion des erreurs »L’intégration inclut une gestion des erreurs intégrée avec des réponses d’erreur typées. Un type <operation-name>Error est généré qui encapsule les réponses d’erreur possibles définies dans la spécification OpenAPI. Chaque erreur a une propriété status et error, et en vérifiant la valeur de status, vous pouvez réduire à un type d’erreur spécifique.
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: 'New Item' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> <p>Trace ID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Gestion des erreurs utilisant directement le 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 is typed as 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 is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as 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>;}Consommation d’un flux
Section intitulée « Consommation d’un flux »Si vous avez configuré votre FastAPI pour diffuser des réponses, votre hook useQuery mettra automatiquement à jour ses données à mesure que de nouveaux morceaux du flux arrivent.
Par exemple :
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> );}Vous pouvez utiliser les propriétés isLoading et fetchStatus pour déterminer l’état actuel du flux si nécessaire. Un flux suit ce cycle de vie :
-
La requête HTTP pour démarrer le streaming est envoyée
isLoadingesttruefetchStatusest'fetching'dataestundefined
-
Le premier morceau du flux est reçu
isLoadingdevientfalsefetchStatusreste'fetching'datadevient un tableau contenant le premier morceau
-
Les morceaux suivants sont reçus
isLoadingrestefalsefetchStatusreste'fetching'dataest mis à jour avec chaque morceau suivant dès qu’il est reçu
-
Le flux se termine
isLoadingrestefalsefetchStatusdevient'idle'dataest un tableau de tous les morceaux reçus
Streaming utilisant directement le client API
Si vous avez configuré votre FastAPI pour diffuser des réponses>, le client généré inclura des méthodes type-safe pour itérer de manière asynchrone sur les morceaux de votre flux en utilisant la syntaxe for await.
Par exemple :
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> );}Personnalisation du code généré
Section intitulée « Personnalisation du code généré »Requêtes et mutations
Section intitulée « Requêtes et mutations »Par défaut, les opérations dans votre FastAPI qui utilisent les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, et toutes les autres sont considérées comme des requêtes.
Vous pouvez modifier ce comportement en utilisant x-query et x-mutation.
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...Le hook généré fournira queryOptions même s’il utilise la méthode HTTP POST :
const items = useQuery(api.listItems.queryOptions());x-mutation
Section intitulée « x-mutation »@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...Le hook généré fournira mutationOptions même s’il utilise la méthode HTTP GET :
// Generated hook will include the custom optionsconst startProcessing = useMutation(api.startProcessing.mutationOptions());Curseur de pagination personnalisé
Section intitulée « Curseur de pagination personnalisé »Par défaut, les hooks générés supposent une pagination basée sur le curseur avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement en utilisant l’extension x-cursor :
@app.get( "/items", openapi_extra={ # Specify a different parameter name for the cursor "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }Si vous ne souhaitez pas générer infiniteQueryOptions pour une opération, vous pouvez définir x-cursor sur False :
@app.get( "/items", openapi_extra={ # Disable cursor-based pagination for this endpoint "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }Regroupement des opérations
Section intitulée « Regroupement des opérations »Les hooks et méthodes client générés sont automatiquement organisés en fonction des tags OpenAPI dans vos endpoints FastAPI. Cela aide à garder vos appels API organisés et facilite la recherche d’opérations liées.
Par exemple :
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@app.get( "/users", tags=["users"],)def list(): # ...Les hooks générés seront regroupés par ces tags :
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Items operations are grouped under api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users const users = useQuery(api.users.list.queryOptions());
// Usage example const handleCreateItem = () => { createItem.mutate({ name: 'New Item' }); };
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Ce regroupement facilite l’organisation de vos appels API et fournit une meilleure complétion de code dans votre IDE.
Opérations regroupées utilisant directement le 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);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations are grouped under 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 { // Create item using the 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> );}Vous pouvez personnaliser les réponses d’erreur dans votre FastAPI en définissant des classes d’exception personnalisées, des gestionnaires d’exceptions et en spécifiant des modèles de réponse pour différents codes d’état d’erreur. Le client généré gérera automatiquement ces types d’erreur personnalisés.
Définition de modèles d’erreur personnalisés
Section intitulée « Définition de modèles d’erreur personnalisés »Tout d’abord, définissez vos modèles d’erreur en utilisant Pydantic :
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]Création d’exceptions personnalisées
Section intitulée « Création d’exceptions personnalisées »Ensuite, créez des classes d’exception personnalisées pour différents scénarios d’erreur :
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = detailsAjout de gestionnaires d’exceptions
Section intitulée « Ajout de gestionnaires d’exceptions »Enregistrez des gestionnaires d’exceptions pour convertir vos exceptions en réponses 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(), )Spécification des modèles de réponse
Section intitulée « Spécification des modèles de réponse »Enfin, spécifiez les modèles de réponse pour différents codes d’état d’erreur dans vos définitions d’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)Utilisation de types d’erreur personnalisés dans React
Section intitulée « Utilisation de types d’erreur personnalisés dans React »Le client généré gérera automatiquement ces types d’erreur personnalisés, vous permettant de vérifier le type et de gérer différentes réponses d’erreur :
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the responses in your FastAPI switch (error.status) { case 404: // error.error is a string as specified in the responses console.error('Not found:', error.error); break; case 500: // error.error is typed as ErrorDetails console.error('Server error:', error.error.message); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as ValidationError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error is a string as specified in the responses console.error('Forbidden:', error.error); break; } } });
// Component rendering with error handling if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}Gestion des erreurs personnalisées avec le client directement
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 with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the responses in your FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is a string as specified in the responses console.error('Not found:', err.error); break; case 500: // err.error is typed as ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with 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 is typed as ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error is a string as specified in the responses console.error('Forbidden:', err.error); break; } } };
// Component rendering with 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> );}Bonnes pratiques
Section intitulée « Bonnes pratiques »Gérer les états de chargement
Section intitulée « Gérer les états de chargement »Gérez toujours les états de chargement et d’erreur pour une meilleure expérience utilisateur :
import { useQuery } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const items = useQuery(api.listItems.queryOptions());
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { const err = items.error; switch (err.status) { case 403: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Gérer les états de chargement utilisant directement le client API
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 is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Mises à jour optimistes
Section intitulée « Mises à jour optimistes »Implémentez des mises à jour optimistes pour une meilleure expérience utilisateur :
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Query to fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, context) => { // If the mutation fails, use the context returned from onMutate to roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Always refetch after error or success to ensure data is in sync with server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Failed to load items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}Mises à jour optimistes utilisant directement le client API
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error 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> );}Sécurité des types
Section intitulée « Sécurité des types »L’intégration fournit une sécurité des types complète de bout en bout. Votre IDE fournira une autocomplétion complète et une vérification de type pour tous vos appels API :
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation for creating items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error if onSuccess callback doesn't handle the correct response type onSuccess: (data) => { // data is fully typed based on your API's response schema console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error if input doesn't match schema createItem.mutate(data); };
// Error UI can use type narrowing to handle different error types if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error is typed as CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error is typed as CreateItem5XXResponse for 500, 502, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}Sécurité des types utilisant directement le client API
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error is typed as CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error is typed as CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types 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>;}Les types sont automatiquement générés à partir du schéma OpenAPI de votre FastAPI, garantissant que toute modification de votre API est reflétée dans votre code frontend après un build.
Authentification personnalisée
Section intitulée « Authentification personnalisée »Si votre FastAPI utilise l’authentification Custom (Lambda Authorizer), vous devrez modifier le provider client généré pour ajouter les en-têtes d’autorisation attendus par votre authorizer. Recherchez la configuration fetch dans le <ApiName>Provider.tsx généré et ajoutez votre token ou clé API aux en-têtes de requête.