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’erreurs appropriée.

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

  1. D’un fichier main.tsx qui rend votre application
  2. D’un backend FastAPI fonctionnel (généré via le générateur FastAPI)
  3. D’une authentification Cognito ajoutée via le générateur ts#react-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 qui génère la spécification OpenAPI pour votre API
    • project.json Une nouvelle cible est ajoutée à la compilation pour invoquer le script de génération ci-dessus

    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 l’é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 vous avez sélectionné l’authentification IAM)
    • project.json Une nouvelle cible est ajoutée à la compilation 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 également une Configuration Runtime à votre infrastructure de site web si elle n’est pas déjà présente, garantissant que l’URL de l’API FastAPI est disponible dans le site et configurée automatiquement par le hook use<ApiName>.tsx.

    Lors de la compilation, 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 définis dans 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 permettant d’interagir avec votre API via TanStack Query

    Le client typé généré peut être utilisé pour appeler votre FastAPI depuis votre application React. Il est recommandé d’utiliser les hooks TanStack Query, mais vous pouvez aussi utiliser le client vanilla si vous préférez.

    watch-generate:<ApiName>-client utilise la commande nx watch, qui nécessite que le Nx Daemon soit actif. Si vous avez désactivé le daemon, le client ne se régénérera pas automatiquement lors des modifications du FastAPI.

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

    Vous pouvez utiliser la méthode queryOptions pour récupérer les options nécessaires à l’appel de votre API via 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>;
    }
    Cliquez ici pour un exemple utilisant le client vanilla directement.

    Les hooks générés incluent la prise en charge des mutations via le hook useMutation de TanStack Query. Cela permet de gérer les opérations de création, mise à jour et suppression avec des états de chargement, la gestion d’erreurs et des mises à jour optimistes.

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    // Crée une mutation utilisant les options générées
    const createItem = useMutation(api.createItem.mutationOptions());
    const handleSubmit = (e) => {
    e.preventDefault();
    createItem.mutate({ name: 'New Item', description: 'A new item' });
    };
    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 aussi ajouter des callbacks pour différents états de mutation :

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    // Exécuté quand la mutation réussit
    console.log('Élément créé :', data);
    // Navigation vers le nouvel élément
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Exécuté quand la mutation échoue
    console.error('Échec de la création :', error);
    },
    onSettled: () => {
    // Exécuté quand la mutation est terminée (succès ou erreur)
    // Idéal pour invalider les requêtes concernées
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Cliquez ici pour un exemple utilisant le client directement.

    Pour les endpoints acceptant un paramètre cursor en entrée, les hooks générés prennent en charge les requêtes infinies via le hook useInfiniteQuery de TanStack Query. Cela facilite 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, // Nombre d'éléments par page
    }, {
    // Définissez une fonction getNextPageParam pour retourner
    // le paramètre à passer comme 'cursor' pour la page suivante
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    {/* Aplatit le tableau de pages pour afficher tous les éléments */}
    <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'
    : 'Aucun élément supplémentaire'}
    </button>
    </div>
    );
    }

    Les hooks générés gèrent automatiquement la pagination basée sur le cursor si votre API la supporte. La valeur nextCursor est extraite de la réponse et utilisée pour récupérer la page suivante.

    Cliquez ici pour un exemple utilisant le client directement.

    L’intégration inclut une gestion d’erreurs avec des réponses d’erreur typées. Un type <operation-name>Error est généré, encapsulant les réponses d’erreur possibles définies dans la spécification OpenAPI. Chaque erreur possède des propriétés status et error, et en vérifiant la valeur de status, vous pouvez cibler 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 est typé comme CreateItem400Response
    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:
    // error.error est typé comme CreateItem403Response
    return (
    <div>
    <h2>Non autorisé :</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error est typé comme CreateItem5XXResponse
    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 vanilla directement.

    Si vous avez configuré votre FastAPI pour diffuser des réponses, votre hook useQuery mettra automatiquement à jour ses données à chaque réception de nouveaux chunks du flux.

    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. Un flux suit ce cycle :

    1. La requête HTTP pour démarrer le streaming est envoyée

      • isLoading est true
      • fetchStatus est 'fetching'
      • data est undefined
    2. Le premier chunk du flux est reçu

      • isLoading devient false
      • fetchStatus reste 'fetching'
      • data devient un tableau contenant le premier chunk
    3. Les chunks suivants sont reçus

      • isLoading reste false
      • fetchStatus reste 'fetching'
      • data est mis à jour avec chaque nouveau chunk reçu
    4. Le flux se termine

      • isLoading reste false
      • fetchStatus devient 'idle'
      • data est un tableau de tous les chunks reçus
    Cliquez ici pour un exemple utilisant le client vanilla directement.

    Par défaut, les opérations FastAPI utilisant les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, les autres comme des requêtes.

    Vous pouvez modifier ce comportement avec 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 avec la méthode HTTP POST :

    const items = useQuery(api.listItems.queryOptions());
    @app.get(
    "/start-processing",
    openapi_extra={
    "x-mutation": True
    }
    )
    def start_processing():
    # ...

    Le hook généré fournira mutationOptions même avec la méthode HTTP GET :

    // Le hook généré inclura les options personnalisées
    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Par défaut, les hooks générés supposent une pagination par cursor avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement via l’extension x-cursor :

    @app.get(
    "/items",
    openapi_extra={
    # Spécifie un nom de paramètre différent pour le cursor
    "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 génération de infiniteQueryOptions pour une opération, définissez x-cursor sur False :

    @app.get(
    "/items",
    openapi_extra={
    # Désactive la pagination par cursor pour cet endpoint
    "x-cursor": False
    }
    )
    def list_items(page: int = 1, limit: int = 10):
    # ...
    return {
    "items": items,
    "total": total_count,
    "page": page,
    "pages": total_pages
    }

    Les hooks et méthodes client générés sont organisés automatiquement selon les tags OpenAPI de vos endpoints FastAPI. Cela facilite l’organisation des appels API.

    Exemple :

    items.py
    @app.get(
    "/items",
    tags=["items"],
    )
    def list():
    # ...
    @app.post(
    "/items",
    tags=["items"],
    )
    def create(item: Item):
    # ...
    users.py
    @app.get(
    "/users",
    tags=["users"],
    )
    def list():
    # ...

    Les hooks générés seront groupés par ces tags :

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Les opérations items sont groupées sous api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Les opérations users sont groupées sous api.users
    const users = useQuery(api.users.list.queryOptions());
    // Exemple d'utilisation
    const handleCreateItem = () => {
    createItem.mutate({ name: 'New Item' });
    };
    return (
    <div>
    <h2>Éléments</h2>
    <ul>
    {items.data?.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    <button onClick={handleCreateItem}>Ajouter un élément</button>
    <h2>Utilisateurs</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 améliore l’autocomplétion dans votre IDE.

    Cliquez ici pour un exemple utilisant le client directement.

    Vous pouvez personnaliser les réponses d’erreur dans FastAPI en définissant des classes d’exception personnalisées, des gestionnaires d’exception, et en spécifiant des modèles de réponse pour différents codes de statut. Le client généré gérera automatiquement ces types d’erreur.

    Définissez d’abord vos modèles d’erreur avec Pydantic :

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

    Créez des classes d’exception pour différents scénarios :

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    Enregistrez des gestionnaires pour convertir vos exceptions en réponses HTTP :

    main.py
    from fastapi import Request
    from 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(),
    )

    Enfin, spécifiez les modèles de réponse pour différents codes d’erreur dans vos endpoints :

    main.py
    @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"Élément {item_id} introuvable")
    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="Données invalides",
    field_errors=["Le nom est requis"]
    )
    )
    return save_item(item)

    Le client généré gérera ces types d’erreur, permettant une vérification de type et une gestion des réponses d’erreur :

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // Requête avec gestion d'erreur typée
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // L'erreur est typée selon les réponses FastAPI
    switch (error.status) {
    case 404:
    // error.error est un string comme spécifié
    console.error('Introuvable :', error.error);
    break;
    case 500:
    // error.error est typé comme ErrorDetails
    console.error('Erreur serveur :', error.error.message);
    break;
    }
    }
    });
    // Mutation avec gestion d'erreur typée
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error est typé comme ValidationError
    console.error('Erreur de validation :', error.error.message);
    console.error('Erreurs de champ :', error.error.field_errors);
    break;
    case 403:
    // error.error est un string
    console.error('Interdit :', error.error);
    break;
    }
    }
    });
    // Rendu avec gestion d'erreur
    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 du composant */}
    </div>
    );
    }
    Cliquez ici pour un exemple utilisant le client directement.

    Toujours gérer 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 est typé comme ListItems403Response
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error est typé comme ListItems5XXResponse
    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 vanilla 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();
    // Requête pour récupérer les éléments
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // Mutation de suppression avec mises à jour optimistes
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // Annule les refetch en cours
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Capture l'état précédent
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Mise à jour optimiste
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Retourne le contexte avec l'état précédent
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // En cas d'échec, restaure l'état précédent
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Échec de la suppression :', err);
    },
    onSettled: () => {
    // Rafraîchit les données après succès ou erreur
    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 fournit une sécurité de type complète. Votre IDE fournira l’autocomplétion et la vérification de type pour tous vos appels API :

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // Mutation typée pour la création d'éléments
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ Erreur de type si le callback onSuccess ne gère pas le bon type
    onSuccess: (data) => {
    // data est typé selon le schéma de réponse de l'API
    console.log(`Élément créé avec l'ID : ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    // ✅ Erreur de type si l'entrée ne correspond pas au schéma
    createItem.mutate(data);
    };
    // UI d'erreur utilisant le narrowing de type
    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: 'New Item' });
    }}>
    {/* Champs du formulaire */}
    <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 automatiquement à 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 une compilation.