Aller au contenu

Réagir à l'API Smithy

Le générateur api-connection permet d’intégrer rapidement votre site React avec votre backend d’API Smithy TypeScript. Il configure tous les éléments nécessaires pour se connecter à votre API Smithy 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 d’API Smithy TypeScript fonctionnel (généré via le générateur ts#smithy-api)
  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 application React :

    • Répertoiresrc
      • Répertoirecomponents
        • <ApiName>Provider.tsx Provider pour le client de votre API
        • QueryClientProvider.tsx Provider du client React Query de TanStack
        • RépertoireRuntimeConfig/ Composant de configuration runtime pour le développement local
      • 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 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 un fichier à votre modèle Smithy :

    • Répertoiremodel
      • Répertoiresrc
        • extensions.smithy Définit des traits pour personnaliser le client généré

    Le générateur ajoutera aussi une Configuration Runtime à votre infrastructure de site si elle n’existe pas déjà, garantissant que l’URL de votre API Smithy 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 API Smithy. 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 structures du modèle Smithy
          • 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é peut être utilisé pour appeler votre API Smithy 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 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>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 via le hook useMutation de TanStack Query. Cela permet de gérer les opérations de création, mise à jour et suppression avec états de chargement, gestion d’erreurs et 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 aussi 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 le client directement.

    Pour les endpoints acceptant un paramètre cursor, les hooks générés prennent en charge les requêtes infinies via 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>
    );
    }

    Les hooks générés gèrent automatiquement la pagination par curseur si votre API la supporte. La valeur nextCursor est extraite de la réponse pour récupérer la page suivante.

    Cliquez ici pour un exemple utilisant le client directement.

    L’intégration inclut une gestion d’erreurs typée. Un type <operation-name>Error est généré, encapsulant les réponses d’erreur possibles définies dans le modèle Smithy. Chaque erreur a une propriété status et error, permettant de 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: '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>
    </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>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Créer l'élément</button>;
    }
    Cliquez ici pour un exemple utilisant le client directement.

    Des traits Smithy sont ajoutés à votre projet Smithy dans extensions.smithy pour personnaliser le client généré.

    Par défaut, les opérations utilisant les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations. Ce comportement peut être modifié avec les traits @query et @mutation.

    Appliquez le trait @query pour forcer le traitement en requête :

    @http(method: "POST", uri: "/items")
    @query
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }

    Appliquez le trait @mutation pour forcer le traitement en mutation :

    @http(method: "GET", uri: "/start-processing")
    @mutation
    operation StartProcessing {
    input: StartProcessingInput
    output: StartProcessingOutput
    }

    Par défaut, le client suppose un paramètre de pagination nommé cursor. Utilisez le trait @cursor pour personnaliser ce comportement :

    @http(method: "GET", uri: "/items")
    @cursor(inputToken: "nextToken")
    operation ListItems {
    input := {
    nextToken: String
    limit: Integer
    }
    output := {
    items: ItemList
    nextToken: String
    }
    }

    Désactivez la pagination :

    @cursor(enabled: false)
    operation ListItems {
    input := {
    cursor: String
    }
    output := {
    ...
    }
    }

    Les hooks et méthodes client sont organisés selon le trait @tags des opérations Smithy. Les opérations partageant les mêmes tags sont regroupées.

    Exemple de modèle Smithy :

    service MyService {
    operations: [ListItems, CreateItem, ListUsers, CreateUser]
    }
    @tags(["items"])
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }
    @tags(["users"])
    operation ListUsers {
    input: ListUsersInput
    output: ListUsersOutput
    }

    Utilisation dans React :

    function ItemsAndUsers() {
    const api = useMyApi();
    const items = useQuery(api.items.listItems.queryOptions());
    const users = useQuery(api.users.listUsers.queryOptions());
    return (
    <div>
    <h2>Éléments</h2>
    <ul>
    {items.data?.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    <h2>Utilisateurs</h2>
    <ul>
    {users.data?.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }
    Cliquez ici pour un exemple utilisant le client directement.

    Définissez des structures d’erreur personnalisées dans votre modèle Smithy pour une gestion typée :

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    fieldErrors: FieldErrorList
    }
    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    errors: [InvalidRequestError, UnauthorizedError]
    }

    Utilisation dans React :

    function ItemComponent() {
    const api = useMyApi();
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    console.error('Erreur de validation :', error.error.fieldErrors);
    break;
    case 403:
    console.error('Non autorisé :', error.error.reason);
    break;
    }
    }
    });
    }

    Toujours gérer les états de chargement et d’erreur :

    function ItemList() {
    const api = useMyApi();
    const items = useQuery(api.listItems.queryOptions());
    if (items.isLoading) return <LoadingSpinner />;
    if (items.isError) {
    switch (items.error.status) {
    case 403:
    return <ErrorMessage message={items.error.error.reason} />;
    case 500:
    return <ErrorMessage message={items.error.error.message} />;
    }
    }
    return (
    <ul>
    {items.data.map((item) => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    );
    }

    Implémentez des mises à jour optimistes pour une meilleure expérience utilisateur :

    function ItemList() {
    const queryClient = useQueryClient();
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    await queryClient.cancelQueries(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);
    }
    });
    }

    L’intégration assure une sécurité des types de bout en bout. Votre IDE fournira l’autocomplétion et le typage pour tous les appels d’API :

    function ItemForm() {
    const api = useMyApi();
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    console.log(`ID de l'élément : ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    createItem.mutate(data);
    };
    if (createItem.error) {
    switch (createItem.error.status) {
    case 400:
    return <FormError errors={createItem.error.error.fieldErrors} />;
    case 403:
    return <AuthError reason={createItem.error.error.reason} />;
    }
    }
    return <form onSubmit={handleSubmit}>{/* ... */}</form>;
    }

    Les types sont générés automatiquement à partir du schéma OpenAPI de votre API Smithy, garantissant que toute modification est reflétée dans le code frontend après compilation.