Aller au contenu

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.

Avant d’utiliser ce générateur, assurez-vous que votre application React dispose de :

  1. Un fichier main.tsx qui rend votre application
  2. Un backend FastAPI fonctionnel (généré via le générateur FastAPI)
  3. 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>,
);
  1. Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
  2. Ouvrez la console Nx dans VSCode
  3. Cliquez sur Generate (UI) dans la section "Common Nx Commands"
  4. Recherchez @aws/nx-plugin - api-connection
  5. Remplissez les paramètres requis
    • Cliquez sur Generate
    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

    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.

    À 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

    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.

    Le générateur fournit un hook use<ApiName> pour appeler votre API avec TanStack Query.

    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>;
    }
    Cliquez ici pour un exemple utilisant le client vanilla directement.

    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() });
    }
    });
    Cliquez ici pour un exemple utilisant le client directement.

    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.

    Cliquez ici pour un exemple utilisant le client directement.

    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>;
    }
    Cliquez ici pour un exemple utilisant le client directement.

    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 :

    1. La requête HTTP est envoyée

      • isLoading : true
      • fetchStatus : 'fetching'
      • data : undefined
    2. Premier chunk reçu

      • isLoading : false
      • fetchStatus : 'fetching'
      • data : tableau avec le premier chunk
    3. Chunks suivants

      • isLoading : false
      • fetchStatus : 'fetching'
      • data : mis à jour à chaque chunk
    4. Flux terminé

      • isLoading : false
      • fetchStatus : 'idle'
      • data : tous les chunks reçus
    Cliquez ici pour un exemple utilisant le client directement.

    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.

    @app.get(
    "/start-processing",
    openapi_extra={
    "x-mutation": True
    }
    )
    def start_processing():
    # ...

    Génère des mutationOptions pour une méthode GET.

    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.

    Les hooks et méthodes client sont organisés selon les tags OpenAPI de vos endpoints FastAPI.

    Exemple :

    items.py
    @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.

    Cliquez ici pour un exemple utilisant le client directement.

    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>;
    }
    Cliquez ici pour un exemple utilisant le client directement.

    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>
    );
    }
    Cliquez ici pour un exemple utilisant le client directement.

    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>
    );
    }
    Cliquez ici pour un exemple utilisant le client directement.

    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>
    );
    }
    Cliquez ici pour un exemple utilisant le client directement.

    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.