Réagir à FastAPI
Le générateur api-connection
permet d’intégrer rapidement votre site React avec votre backend FastAPI. Il configure tous les éléments nécessaires pour connecter vos backends FastAPI de manière typée, incluant la génération de client et de hooks TanStack Query, le support de l’authentification AWS IAM et une gestion d’erreurs appropriée.
Prérequis
Avant d’utiliser ce générateur, assurez-vous que votre application React possède :
- Un fichier
main.tsx
qui rend votre application - Un backend FastAPI fonctionnel (généré avec le générateur FastAPI)
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
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
Options
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 |
auth | string | IAM | Authentication strategy (choose from IAM or None) |
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 la spécification OpenAPI pour votre API
- project.json Une nouvelle cible est ajoutée à la build pour exécuter 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 l’authentification IAM est sélectionnée)
- project.json Une nouvelle cible est ajoutée à la build pour générer un client typé
- .gitignore Les fichiers clients générés sont ignorés par défaut
Le générateur ajoutera aussi une configuration Runtime à votre infrastructure de site si absente, 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
Lors de la build, un client typé 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 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é
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
Le générateur fournit un hook use<ApiName>
pour appeler votre API avec TanStack Query.
Requêtes
Utilisez queryOptions
pour récupérer les options nécessaires à 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>;}
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>Chargement...</div>; if (error) return <div>Erreur : {error.message}</div>;
return <div>Élément : {item.name}</div>;}
Mutations
Les hooks générés supportent 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> );}
Vous pouvez ajouter 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 la création :', error); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutations avec le client API 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 la 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
Pour les endpoints utilisant un paramètre cursor
, les hooks générés supportent les requêtes infinies avec useInfiniteQuery
de TanStack Query, facilitant l’implémentation de fonctionnalités “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, }, { 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 nextCursor
est extrait automatiquement de la réponse pour récupérer la page suivante.
Pagination avec le client API 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
L’intégration inclut une gestion d’erreurs typée. Le type <operation-name>Error
encapsule les réponses d’erreur possibles. En vérifiant le status
, vous pouvez traiter chaque 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: '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 API 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>;}
Consommation d’un flux
Si vous avez configuré le streaming dans FastAPI, le hook useQuery
mettra automatiquement à jour ses données à l’arrivée de nouveaux chunks.
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 :
-
Envoi de la requête HTTP
isLoading
:true
fetchStatus
:'fetching'
data
:undefined
-
Réception du premier chunk
isLoading
:false
fetchStatus
:'fetching'
data
: tableau avec le premier chunk
-
Réception des chunks suivants
isLoading
:false
fetchStatus
:'fetching'
data
: mis à jour à chaque nouveau chunk
-
Fin du flux
isLoading
:false
fetchStatus
:'idle'
data
: tableau complet des chunks
Streaming avec le client API 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> );}
Personnalisation du code généré
Requêtes et mutations
Par défaut, les méthodes HTTP PUT
, POST
, PATCH
et DELETE
sont considérées comme des mutations. Ce comportement peut être modifié avec x-query
et x-mutation
.
x-query
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
Génère des queryOptions
pour une méthode POST :
const items = useQuery(api.listItems.queryOptions());
x-mutation
@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
Génère des mutationOptions
pour une méthode GET :
const startProcessing = useMutation(api.startProcessing.mutationOptions());
Curseur de pagination personnalisé
Par défaut, le paramètre de pagination est nommé cursor
. Personnalisez-le 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 }
Pour désactiver la pagination :
@app.get( "/items", openapi_extra={ "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
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 :
const api = useMyApi();
// Opérations groupées sous api.itemsconst items = useQuery(api.items.list.queryOptions());const createItem = useMutation(api.items.create.mutationOptions());
// Opérations groupées sous api.usersconst users = useQuery(api.users.list.queryOptions());
Regroupement avec le client API directement
import { useMyApiClient } from './hooks/useMyApiClient';
// Opérations groupées sous api.itemsconst itemsData = await api.items.list();const newItem = await api.items.create({ name: 'Nouvel élément' });
// Opérations groupées sous api.usersconst usersData = await api.users.list();
Erreurs
Personnalisez les réponses d’erreur avec des modèles d’exception et des gestionnaires. Le client généré gérera ces types automatiquement.
Définition de modèles d’erreur
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
Création d’exceptions
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
Gestionnaires d’exceptions
@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
@app.get( "/items/{item_id}", responses={ 404: {"model": str}, 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: # ...
Utilisation dans React
Le client gère les types d’erreur personnalisés :
switch (error.status) { case 404: console.error('Non trouvé :', error.error); break; case 500: console.error('Erreur serveur :', error.error.message); break;}
Gestion d'erreurs personnalisées avec le client directement
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; }}
Bonnes pratiques
Gestion des états de chargement
Toujours gérer les états de chargement et d’erreur :
if (items.isLoading) { return <LoadingSpinner />;}
if (items.isError) { return <ErrorMessage message="Échec du chargement" />;}
Gestion des états avec le client directement
if (loading) { return <LoadingSpinner />;}
if (error) { return <ErrorMessage message={error.message} />;}
Mises à jour optimistes
Implémentez des mises à jour optimistes pour une meilleure expérience utilisateur :
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); },});
Mises à jour optimistes avec le client directement
const handleDelete = async (itemId) => { const previousItems = items; setItems(items.filter((item) => item.id !== itemId)); try { await api.deleteItem(itemId); } catch (error) { setItems(previousItems); }};
Sécurité des types
L’intégration assure une sécurité de type complète. Votre IDE fournira de l’autocomplétion et du typage pour tous les appels d’API :
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // data est typé selon le schéma de réponse console.log(`ID : ${data.id}`); },});
const handleSubmit = (data: CreateItemInput) => { // Erreur de type si l'entrée ne correspond pas createItem.mutate(data);};
Sécurité des types avec le client directement
try { // Erreur de type si l'entrée est incorrecte await api.createItem(data);} catch (e) { const err = e as CreateItemError; // Gestion typée des erreurs}
Les types sont générés automatiquement à partir du schéma OpenAPI, reflétant toute modification de l’API après une build.