Réagir à FastAPI
Le générateur api-connection
permet d’intégrer rapidement votre site React avec votre backend FastAPI. Il configure toute la configuration nécessaire pour se connecter à vos backends FastAPI de manière typée, incluant la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito, ainsi qu’une gestion d’erreur appropriée.
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.tsx
qui rend votre application - Un backend FastAPI fonctionnel (généré via le générateur FastAPI)
- Une authentification Cognito ajoutée via le générateur
ts#cloudscape-website-auth
si vous connectez une API utilisant l’authentification Cognito ou IAM
Exemple de structure requise pour 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>,);
Utilisation
Section intitulée « Utilisation »Exécuter le générateur
Section intitulée « Exécuter le générateur »- 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 - api-connection
- Remplissez les paramètres requis
- Cliquez sur
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
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
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
Paramètre | Type | Par défaut | Description |
---|---|---|---|
sourceProject Requis | string | - | The source project which will call the API |
targetProject Requis | string | - | The target project containing your API |
Résultat du générateur
Section intitulée « Résultat du générateur »Le générateur modifiera les fichiers suivants dans votre projet FastAPI :
Répertoirescripts
- generate_open_api.py Ajoute un script générant une spécification OpenAPI pour votre API
- project.json Ajoute une nouvelle cible de build invoquant le script de génération
Le générateur modifiera les fichiers suivants dans votre application React :
Répertoiresrc
Répertoirecomponents
- <ApiName>Provider.tsx Provider pour le client de votre API
- QueryClientProvider.tsx Provider du client TanStack React Query
Répertoirehooks
- use<ApiName>.tsx Ajoute un hook pour appeler votre API avec état géré par TanStack Query
- use<ApiName>Client.tsx Ajoute un hook pour instancier le client vanilla pouvant appeler votre API
- useSigV4.tsx Ajoute un hook pour signer les requêtes HTTP avec SigV4 (si IAM est sélectionné)
- project.json Ajoute une nouvelle cible de build générant un client typé
- .gitignore Les fichiers clients générés sont ignorés par défaut
Le générateur ajoutera également une configuration runtime à votre infrastructure de site si elle n’existe pas déjà, garantissant que l’URL de l’API FastAPI est disponible dans le site et configurée automatiquement par le hook use<ApiName>.tsx
.
Génération de code
Section intitulée « Génération de code »À la compilation, un client typé est généré à partir de la spécification OpenAPI de votre FastAPI. Cela ajoute trois nouveaux fichiers à votre application React :
Répertoiresrc
Répertoiregenerated
Répertoire<ApiName>
- types.gen.ts Types générés depuis les modèles pydantic de votre FastAPI
- client.gen.ts Client typé pour appeler votre API
- options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query interagissant avec votre API
Utiliser le code généré
Section intitulée « Utiliser le code généré »Le client typé généré permet d’appeler votre FastAPI depuis votre application React. Il est recommandé d’utiliser les hooks TanStack Query, mais le client vanilla est aussi disponible.
Utiliser le hook d’API
Section intitulée « Utiliser le hook d’API »Le générateur fournit un hook use<ApiName>
pour appeler votre API avec TanStack Query.
Requêtes
Section intitulée « Requêtes »Utilisez queryOptions
pour récupérer les options nécessaires avec 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>Chargement...</div>; if (item.isError) return <div>Erreur : {item.error.message}</div>;
return <div>Élément : {item.data.name}</div>;}
Utiliser le client d'API directement
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>Chargement...</div>; if (error) return <div>Erreur : {error.message}</div>;
return <div>Élément : {item.name}</div>;}
Mutations
Section intitulée « Mutations »Les hooks générés prennent en charge les mutations avec useMutation
de TanStack Query, gérant les états de chargement, les erreurs et les mises à jour optimistes.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Crée une mutation avec les options générées const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'Nouvel élément', description: 'Un nouvel élément' }); };
return ( <form onSubmit={handleSubmit}> {/* Champs du formulaire */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Création...' : 'Créer l\'élément'} </button>
{createItem.isSuccess && ( <div className="success"> Élément créé avec l'ID : {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Erreur : {createItem.error.message} </div> )} </form> );}
Ajoutez des callbacks pour différents états de mutation :
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { console.log('Élément créé :', data); navigate(`/items/${data.id}`); }, onError: (error) => { console.error('Échec de création :', error); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutations avec le client directement
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: 'Nouvel élément', description: 'Un nouvel élément' }); setCreatedItem(newItem); } catch (err) { setError(err); console.error('Échec de création :', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Champs du formulaire */} <button type="submit" disabled={isLoading} > {isLoading ? 'Création...' : 'Créer l\'élément'} </button>
{createdItem && ( <div className="success"> Élément créé avec l'ID : {createdItem.id} </div> )}
{error && ( <div className="error"> Erreur : {error.message} </div> )} </form> );}
Pagination avec requêtes infinies
Section intitulée « Pagination avec requêtes infinies »Pour les endpoints utilisant un paramètre cursor
, les hooks générés prennent en charge les requêtes infinies avec useInfiniteQuery
pour un 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, }, { getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> <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 ? 'Chargement...' : items.hasNextPage ? 'Charger plus' : 'Fin des éléments'} </button> </div> );}
Le hook gère automatiquement la pagination si votre API supporte un nextCursor
.
Pagination avec le client directement
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);
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]);
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 ? 'Chargement...' : nextCursor ? 'Charger plus' : 'Fin des éléments'} </button> </div> );}
Gestion des erreurs
Section intitulée « Gestion des erreurs »L’intégration inclut une gestion d’erreurs typée. Le type <operation-name>Error
encapsule les réponses d’erreur possibles. Vérifiez la propriété status
pour identifier le type d’erreur.
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: 'Nouvel élément' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: return ( <div> <h2>Entrée invalide :</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: return ( <div> <h2>Non autorisé :</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Erreur serveur :</h2> <p>{createItem.error.error.message}</p> <p>Trace ID : {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Créer l'élément</button>;}
Gestion des erreurs avec le client directement
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'Nouvel élément' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: return ( <div> <h2>Entrée invalide :</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>Non autorisé :</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Erreur serveur :</h2> <p>{error.error.message}</p> <p>Trace ID : {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Créer l'élément</button>;}
Consommer un flux
Section intitulée « Consommer un flux »Si votre FastAPI est configuré pour streamer les réponses, le hook useQuery
mettra à jour ses données à chaque réception de chunk.
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> );}
Le cycle de vie d’un flux :
-
La requête HTTP est envoyée
isLoading
:true
fetchStatus
:'fetching'
data
:undefined
-
Premier chunk reçu
isLoading
:false
fetchStatus
:'fetching'
data
: tableau avec le premier chunk
-
Chunks suivants
isLoading
:false
fetchStatus
:'fetching'
data
: mis à jour à chaque chunk
-
Flux terminé
isLoading
:false
fetchStatus
:'idle'
data
: tous les chunks reçus
Streaming avec le client directement
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> );}
Personnaliser le code généré
Section intitulée « Personnaliser le code généré »Requêtes et mutations
Section intitulée « Requêtes et mutations »Par défaut, les méthodes HTTP PUT
, POST
, PATCH
, DELETE
sont considérées comme des mutations. Modifiez ce comportement avec x-query
et x-mutation
.
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
Génère des queryOptions
pour une méthode POST.
x-mutation
Section intitulée « x-mutation »@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
Génère des mutationOptions
pour une méthode GET.
Curseur de pagination personnalisé
Section intitulée « Curseur de pagination personnalisé »Personnalisez le nom du paramètre de pagination avec x-cursor
:
@app.get( "/items", openapi_extra={ "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }
Désactivez la pagination avec x-cursor: False
.
Regroupement d’opérations
Section intitulée « Regroupement d’opérations »Les hooks et méthodes client sont organisés selon les tags OpenAPI de vos endpoints FastAPI.
Exemple :
@app.get("/items", tags=["items"])def list(): # ...
@app.post("/items", tags=["items"])def create(item: Item): # ...
Les hooks générés seront groupés sous api.items
et api.users
.
Regroupement avec le client directement
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);
useEffect(() => { const fetchData = async () => { try { setIsLoading(true); const itemsData = await api.items.list(); setItems(itemsData); const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Erreur de récupération :', error); } finally { setIsLoading(false); } }; fetchData(); }, [api]);
const handleCreateItem = async () => { try { const newItem = await api.items.create({ name: 'Nouvel élément' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Erreur de création :', error); } };
if (isLoading) return <div>Chargement...</div>;
return ( <div> <h2>Éléments</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Ajouter</button>
<h2>Utilisateurs</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
Personnalisez les réponses d’erreur avec des modèles Pydantic, des exceptions personnalisées et des gestionnaires. Le client généré gérera ces types d’erreur.
Exemple de gestion d’erreurs typées dans React :
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { switch (error.status) { case 404: console.error('Non trouvé :', error.error); break; case 500: console.error('Erreur serveur :', error.error.message); break; } } });
const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: console.error('Erreur de validation :', error.error.message); break; case 403: console.error('Interdit :', error.error); break; } } });
if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return <div>{/* Contenu */}</div>;}
Gestion d'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);
useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { const err = e as GetItemError; setError(err); switch (err.status) { case 404: console.error('Non trouvé :', err.error); break; case 500: console.error('Erreur serveur :', err.error.message); break; } } finally { setLoading(false); } }; fetchItem(); }, [api]);
const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError; switch (err.status) { case 400: console.error('Erreur de validation :', err.error.message); break; case 403: console.error('Interdit :', err.error); break; } } };
if (loading) return <LoadingSpinner />; if (error) { if (error.status === 404) return <NotFoundMessage message={error.error} />; if (error.status === 500) return <ErrorMessage message={error.error.message} />; }
return <div>{/* Contenu */}</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 :
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: return <ErrorMessage message={err.error.reason} />; case 500: return <ErrorMessage message={err.error.message} details={`Trace ID : ${err.error.traceId}`} />; default: return <ErrorMessage message="Erreur inconnue" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Gestion des états avec le client directement
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: return <ErrorMessage message={err.error.reason} />; case 500: return <ErrorMessage message={err.error.message} details={`Trace ID : ${err.error.traceId}`} />; default: return <ErrorMessage message="Erreur inconnue" />; } }
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();
const itemsQuery = useQuery(api.listItems.queryOptions());
const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() }); const previousItems = queryClient.getQueryData(api.listItems.queryKey()); queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) ); return { previousItems }; }, onError: (err, itemId, context) => { queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Échec de suppression :', err); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) return <LoadingSpinner />; if (itemsQuery.isError) return <ErrorMessage message="Échec du chargement" />;
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Suppression...' : 'Supprimer'} </button> </li> ))} </ul> );}
Mises à jour optimistes avec le client directement
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { setItems(previousItems); console.error('Échec de suppression :', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Supprimer</button> </li> ))} </ul> );}
Sécurité des types
Section intitulée « Sécurité des types »L’intégration assure une sécurité des types de bout en bout. Votre IDE fournira l’autocomplétion et la vérification des types pour tous les appels d’API.
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { console.log(`Élément créé avec l'ID : ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { createItem.mutate(data); };
if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: return <FormError message="Entrée invalide" errors={error.error.validationErrors} />; case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'Nouvel élément' }); }}> <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Création...' : 'Créer l\'élément'} </button> </form> );}
Sécurité des types avec le client directement
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError; switch (err.status) { case 400: console.error('Erreurs de validation :', err.error.validationErrors); break; case 403: console.error('Non autorisé :', err.error.reason); break; case 500: console.error('Erreur serveur :', err.error.message, 'Trace :', err.error.traceId); break; } setError(err); } };
if (error) { switch (error.status) { case 400: return <FormError message="Entrée invalide" 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 générés depuis le schéma OpenAPI de votre FastAPI, garantissant que toute modification est reflétée dans le frontend après compilation.