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 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 :

  1. Un fichier main.tsx qui rend votre application
  2. 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

  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

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

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

    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.

    Cliquez ici pour un exemple utilisant directement le client.

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

    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 :

    1. Envoi de la requête HTTP

      • isLoading : true
      • fetchStatus : 'fetching'
      • data : undefined
    2. Réception du premier chunk

      • isLoading : false
      • fetchStatus : 'fetching'
      • data : tableau avec le premier chunk
    3. Réception des chunks suivants

      • isLoading : false
      • fetchStatus : 'fetching'
      • data : mis à jour à chaque nouveau chunk
    4. Fin du flux

      • isLoading : false
      • fetchStatus : 'idle'
      • data : tableau complet des chunks
    Cliquez ici pour un exemple utilisant directement le client.

    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 :

    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 :

    const api = useMyApi();
    // Opérations groupées sous api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Opérations groupées sous api.users
    const users = useQuery(api.users.list.queryOptions());
    Cliquez ici pour un exemple utilisant directement le client.

    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

    models.py
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    Création d’exceptions

    exceptions.py
    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

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

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

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

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

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

    Les types sont générés automatiquement à partir du schéma OpenAPI, reflétant toute modification de l’API après une build.