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.
Prérequis
Section intitulée « Prérequis »Avant d’utiliser ce générateur, assurez-vous que votre application React dispose :
- D’un fichier
main.tsx
qui rend votre application - D’un backend d’API Smithy TypeScript fonctionnel (généré via le générateur
ts#smithy-api
) - 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>,);
Utilisation
Section intitulée « Utilisation »Exécuter le générateur
Section intitulée « Exécuter le générateur »- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)
dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - api-connection
- Remplissez les paramètres requis
- Cliquez sur
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
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 |
Résultat du générateur
Section intitulée « Résultat du générateur »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
.
Génération de code
Section intitulée « Génération de code »À 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
Utiliser le code généré
Section intitulée « Utiliser le code généré »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.
Utiliser le hook d’API
Section intitulée « Utiliser le hook d’API »Le générateur fournit un hook use<ApiName>
pour appeler votre API avec TanStack Query.
Requêtes
Section intitulée « Requêtes »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>;}
Utiliser le client d'API directement
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Chargement...</div>; if (error) return <div>Erreur : {error.message}</div>;
return <div>Élément : {item.name}</div>;}
Mutations
Section intitulée « Mutations »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() }); }});
Mutations avec le client d'API directement
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'Nouvel élément', description: 'Un nouvel élément' }); setCreatedItem(newItem); } catch (err) { setError(err); console.error('Échec de la création :', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> <button type="submit" disabled={isLoading} > {isLoading ? 'Création...' : 'Créer l\'élément'} </button>
{createdItem && ( <div className="success"> Élément créé avec l'ID : {createdItem.id} </div> )}
{error && ( <div className="error"> Erreur : {error.message} </div> )} </form> );}
Pagination avec requêtes infinies
Section intitulée « Pagination avec requêtes infinies »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.
Pagination avec le client d'API directement
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
useEffect(() => { const fetchItems = async () => { try { const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { console.error(err); } }; fetchItems(); }, [api]);
const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { console.error(err); } finally { setIsFetchingMore(false); } };
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Chargement...' : nextCursor ? 'Charger plus' : 'Fin des éléments'} </button> </div> );}
Gestion des erreurs
Section intitulée « Gestion des erreurs »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>;}
Gestion des erreurs avec le client d'API directement
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'Nouvel élément' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: return ( <div> <h2>Entrée invalide :</h2> <p>{error.error.message}</p> </div> ); case 403: return ( <div> <h2>Non autorisé :</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Erreur serveur :</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Créer l'élément</button>;}
Personnalisation du code généré
Section intitulée « Personnalisation du code généré »Des traits Smithy sont ajoutés à votre projet Smithy dans extensions.smithy
pour personnaliser le client généré.
Requêtes et mutations
Section intitulée « Requêtes et mutations »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")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}
@mutation
Section intitulée « @mutation »Appliquez le trait @mutation
pour forcer le traitement en mutation :
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}
Curseur de pagination personnalisé
Section intitulée « Curseur de pagination personnalisé »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 := { ... }}
Regroupement d’opérations
Section intitulée « Regroupement d’opérations »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> );}
Regroupement avec le client d'API directement
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]);
useEffect(() => { const fetchData = async () => { const itemsData = await api.items.listItems(); setItems(itemsData); const usersData = await api.users.listUsers(); setUsers(usersData); }; fetchData(); }, [api]);
return ( <div> <h2>Éléments</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<h2>Utilisateurs</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
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; } } });}
Bonnes pratiques
Section intitulée « Bonnes pratiques »Gérer les états de chargement
Section intitulée « Gérer les états de chargement »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> );}
Mises à jour optimistes
Section intitulée « Mises à jour optimistes »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); } });}
Sécurité des types
Section intitulée « Sécurité des types »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.