Aller au contenu

Réagir à l'API Smithy

Le générateur 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 - connection
  5. Remplissez les paramètres requis
    • Cliquez sur Generate
    Paramètre Type Par défaut Description
    sourceProject Requis string - The source project
    targetProject Requis string - The target project to connect to

    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) => {
    // Ceci s'exécutera lorsque la mutation réussit
    console.log('Élément créé :', data);
    // Vous pouvez naviguer vers le nouvel élément
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Ceci s'exécutera lorsque la mutation échoue
    console.error('Échec de la création :', error);
    },
    onSettled: () => {
    // Ceci s'exécutera lorsque la mutation se termine (succès ou erreur)
    // Bon endroit pour invalider les requêtes qui pourraient être affectées
    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, // Nombre d'éléments par page
    }, {
    // Assurez-vous de définir une fonction getNextPageParam pour retourner
    // le paramètre qui doit être passé 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>
    {/* Aplatir le tableau des 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'
    : '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:
    // error.error est typé comme CreateItem400Response
    return (
    <div>
    <h2>Entrée invalide :</h2>
    <p>{createItem.error.error.message}</p>
    </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>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Créer l'élément</button>;
    }
    Cliquez ici pour un exemple utilisant le client vanilla 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, et toutes les autres comme des requêtes.

    Vous pouvez modifier ce comportement avec les traits Smithy @query et @mutation ajoutés à votre projet de modèle dans extensions.smithy.

    Appliquez le trait @query à votre opération Smithy pour forcer son traitement en requête :

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

    Le hook généré fournira queryOptions même s’il utilise la méthode HTTP POST :

    const items = useQuery(api.listItems.queryOptions());

    Appliquez le trait @mutation à votre opération Smithy pour forcer son traitement en mutation :

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

    Le hook généré fournira mutationOptions même s’il utilise la méthode HTTP GET :

    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Par défaut, les hooks générés supposent une pagination par curseur avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement avec le trait @cursor ajouté à votre projet de modèle dans extensions.smithy.

    Appliquez le trait @cursor avec inputToken pour changer le nom du paramètre d’entrée utilisé pour le jeton de pagination :

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

    Si vous ne souhaitez pas générer infiniteQueryOptions pour une opération ayant un paramètre d’entrée nommé cursor, vous pouvez désactiver la pagination par curseur :

    @cursor(enabled: false)
    operation ListItems {
    input := {
    // Un paramètre d'entrée nommé 'cursor' fera que cette opération sera traitée comme paginée par défaut
    cursor: String
    }
    output := {
    ...
    }
    }

    Les hooks et méthodes client générés sont automatiquement organisés selon le trait @tags de vos opérations Smithy. Les opérations partageant les mêmes tags sont regroupées, ce qui aide à organiser vos appels d’API et améliore l’autocomplétion dans votre IDE.

    Par exemple, avec ce modèle Smithy :

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

    Les hooks générés seront regroupés par tags :

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Les opérations Items sont regroupées sous api.items
    const items = useQuery(api.items.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // Les opérations Users sont regroupées sous api.users
    const users = useQuery(api.users.listUsers.queryOptions());
    // Exemple d'utilisation
    const handleCreateItem = () => {
    createItem.mutate({ name: 'Nouvel élément' });
    };
    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 d’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 votre API Smithy en définissant des structures d’erreur personnalisées dans votre modèle Smithy. Le client généré gérera automatiquement ces types d’erreur personnalisés.

    Définir des structures d’erreur personnalisées

    Section intitulée « Définir des structures d’erreur personnalisées »

    Définissez vos structures d’erreur dans votre modèle Smithy :

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    fieldErrors: FieldErrorList
    }
    @error("client")
    @httpError(403)
    structure UnauthorizedError {
    @required
    reason: String
    }
    @error("server")
    @httpError(500)
    structure InternalServerError {
    @required
    message: String
    traceId: String
    }
    list FieldErrorList {
    member: FieldError
    }
    structure FieldError {
    @required
    field: String
    @required
    message: String
    }

    Spécifiez quelles erreurs vos opérations peuvent retourner :

    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    errors: [
    InvalidRequestError
    UnauthorizedError
    InternalServerError
    ]
    }
    operation GetItem {
    input: GetItemInput
    output: GetItemOutput
    errors: [
    ItemNotFoundError
    InternalServerError
    ]
    }
    @error("client")
    @httpError(404)
    structure ItemNotFoundError {
    @required
    message: String
    }

    Utiliser les types d’erreur personnalisés dans React

    Section intitulée « Utiliser les types d’erreur personnalisés dans React »

    Le client généré gérera automatiquement ces types d’erreur personnalisés, vous permettant de vérifier les types et gérer différentes 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 erreurs de votre modèle Smithy
    switch (error.status) {
    case 404:
    // error.error est typé comme ItemNotFoundError
    console.error('Non trouvé :', error.error.message);
    break;
    case 500:
    // error.error est typé comme InternalServerError
    console.error('Erreur serveur :', error.error.message);
    console.error('ID de trace :', error.error.traceId);
    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 InvalidRequestError
    console.error('Erreur de validation :', error.error.message);
    console.error('Erreurs de champs :', error.error.fieldErrors);
    break;
    case 403:
    // error.error est typé comme UnauthorizedError
    console.error('Non autorisé :', error.error.reason);
    break;
    }
    }
    });
    // Rendu du composant avec gestion d'erreur
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error.message} />;
    } else if (getItem.error.status === 500) {
    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}
    />
    );
    default:
    return <ErrorMessage message="Une erreur inconnue s'est produite" />;
    }
    }
    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 pour supprimer des éléments avec mises à jour optimistes
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // Annuler toutes les récupérations en cours
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Capturer la valeur précédente
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Mettre à jour de façon optimiste vers la nouvelle valeur
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Retourner un objet de contexte avec la capture
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // Si la mutation échoue, utiliser le contexte retourné par onMutate pour annuler
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Échec de la suppression de l\'élément :', err);
    },
    onSettled: () => {
    // Toujours récupérer après erreur ou succès pour s'assurer que les données sont synchronisées avec le serveur
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    },
    });
    if (itemsQuery.isLoading) {
    return <LoadingSpinner />;
    }
    if (itemsQuery.isError) {
    return <ErrorMessage message="Échec du chargement des éléments" />;
    }
    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 vanilla directement.

    L’intégration fournit une sécurité des types de bout en bout complète. Votre IDE fournira l’autocomplétion et la vérification de type pour tous vos appels d’API :

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // Mutation typée pour créer des éléments
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ Erreur de type si le callback onSuccess ne gère pas le bon type de réponse
    onSuccess: (data) => {
    // data est entièrement typé selon le schéma de réponse de votre 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);
    };
    // L'interface d'erreur peut utiliser le rétrécissement de type pour gérer différents types d'erreur
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error est typé comme InvalidRequestError
    return (
    <FormError
    message="Entrée invalide"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error est typé comme UnauthorizedError
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error est typé comme InternalServerError pour 500, etc.
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: 'Nouvel élément' });
    }}>
    {/* 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 vanilla directement.

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