Aller au contenu

React vers API Smithy

Le générateur connection fournit un moyen d’intégrer rapidement votre site web React avec votre backend d’API TypeScript Smithy. Il configure toute la configuration nécessaire pour se connecter à votre API Smithy de manière type-safe, y compris la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito et la gestion appropriée des erreurs.

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

  1. Un fichier main.tsx qui rend votre application
  2. Un backend d’API TypeScript Smithy fonctionnel (généré à l’aide du générateur ts#api avec --framework=smithy)
  3. Cognito Auth ajouté via le générateur ts#website#auth si vous connectez une API qui utilise l’authentification Cognito ou IAM
Exemple de structure main.tsx requise
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>,
);

Exécuter ce générateur@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
Composez votre commande5

Requis

Requis

Options du générateur5 options
sourceProjectRequisstring

Le projet source

targetProjectRequisstring

Le projet cible auquel se connecter

sourceComponentstring

Le composant source depuis lequel se connecter (nom du composant, chemin relatif à la racine du projet source, ou identifiant de générateur). Utilisez '.' pour sélectionner explicitement le projet comme source.

targetComponentstring

Le composant cible auquel se connecter (nom du composant, chemin relatif à la racine du projet cible, ou identifiant de générateur). Utilisez '.' pour sélectionner explicitement le projet comme cible.

preferInstallDependenciesbooleanPar défaut: true

Indique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin.

Le générateur apportera des modifications aux fichiers suivants dans votre application React :

  • Répertoiresrc
    • Répertoirecomponents
      • <ApiName>Provider.tsx Provider pour votre client API
      • QueryClientProvider.tsx Provider de client TanStack React Query
      • RépertoireRuntimeConfig/ Composant de configuration d’exécution pour le développement local
    • 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 API vanilla qui peut 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 au build qui génère un client type-safe
  • .gitignore Les fichiers client 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 les traits qui peuvent être utilisés pour personnaliser le client généré

Le générateur ajoutera également Runtime Config à votre infrastructure de site web si elle n’est pas déjà présente, ce qui garantit que l’URL de l’API pour votre API Smithy est disponible dans le site web et automatiquement configurée par le hook use<ApiName>.tsx.

Au moment de la compilation, un client type-safe 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 type-safe pour appeler votre API
        • options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query pour interagir avec votre API en utilisant TanStack Query

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

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 requises pour appeler votre API en utilisant 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.isPending) return <div>Loading...</div>;
if (item.isError) return <div>Error: {JSON.stringify(item.error.error)}</div>;
return <div>Item: {item.data.name}</div>;
}

L’erreur est une union discriminée d’objets { status, error } plutôt qu’une Error, il n’y a donc pas de message de niveau supérieur à afficher. Réduisez sur status pour lire un corps de réponse spécifique — voir Gestion des erreurs ci-dessous.

Cliquez ici pour un exemple utilisant directement le client vanilla.

Les hooks générés incluent la prise en charge des mutations à l’aide du hook useMutation de TanStack Query. Cela fournit un moyen propre de gérer les opérations de création, de mise à jour et de suppression avec des états de chargement, la gestion des erreurs et les mises à jour optimistes.

import { useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() {
const api = useMyApi();
// Create a mutation using the generated mutation options
const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => {
e.preventDefault();
createItem.mutate({ name: 'New Item', description: 'A new item' });
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button
type="submit"
disabled={createItem.isPending}
>
{createItem.isPending ? 'Creating...' : 'Create Item'}
</button>
{createItem.isSuccess && (
<div className="success">
Item created with ID: {createItem.data.id}
</div>
)}
{createItem.isError && (
<div className="error">
Error: {JSON.stringify(createItem.error.error)}
</div>
)}
</form>
);
}

Vous pouvez également ajouter des callbacks pour différents états de mutation :

const createItem = useMutation({
...api.createItem.mutationOptions(),
onSuccess: (data) => {
// This will run when the mutation succeeds
console.log('Item created:', data);
// You can navigate to the new item
navigate(`/items/${data.id}`);
},
onError: (error) => {
// This will run when the mutation fails
console.error('Failed to create item:', error);
},
onSettled: () => {
// This will run when the mutation completes (success or error)
// Good place to invalidate queries that might be affected
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
}
});
Cliquez ici pour un exemple utilisant directement le client.

Pour les endpoints qui acceptent un paramètre cursor en entrée, les hooks générés fournissent la prise en charge des requêtes infinies à l’aide du hook useInfiniteQuery de TanStack Query. Cela facilite l’implémentation de la fonctionnalité “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, // Number of items per page
}, {
// Make sure you define a getNextPageParam function to return
// the parameter that should be passed as the 'cursor' for the
// next page
getNextPageParam: (lastPage) =>
lastPage.nextCursor || undefined
}),
});
if (items.isPending) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={JSON.stringify(items.error.error)} />;
}
return (
<div>
{/* Flatten the pages array to render all items */}
<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
? 'Loading more...'
: items.hasNextPage
? 'Load More'
: 'No more items'}
</button>
</div>
);
}

Les hooks générés gèrent automatiquement la pagination basée sur le curseur si votre API la prend en charge. 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 directement le client.

L’intégration inclut une gestion des erreurs intégrée avec des réponses d’erreur typées. Un type <OperationName>Error est généré qui est une union d’un membre <OperationName><StatusCode>Error par statut d’erreur que l’opération modélise. Chaque membre a une propriété status et une propriété error, donc en vérifiant status, vous réduisez error à la charge utile de ce statut.

L’exemple ci-dessous suppose que CreateItem modélise les structures d’erreur définies plus bas dans cette page — une InvalidRequestError 422, une UnauthorizedError 403 et une InternalServerError 500. Seuls les statuts que votre modèle déclare apparaissent dans l’union, donc un case pour un statut que vous n’avez pas modélisé est une erreur de compilation.

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 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
// error.error is typed as InternalServerErrorResponseContent
return (
<div>
<h2>Server error:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</button>;
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

Une sélection de traits Smithy est ajoutée à votre projet Smithy model cible dans extensions.smithy que vous pouvez utiliser pour personnaliser le client généré.

Par défaut, les opérations de votre API Smithy qui utilisent les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, et toutes les autres sont considérées comme des requêtes.

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

Appliquez le trait @query à votre opération Smithy pour la forcer à être traitée comme une requête :

@http(method: "POST", uri: "/search-items")
@query
operation SearchItems {
input: SearchItemsInput
output: SearchItemsOutput
}

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

const items = useQuery(api.searchItems.queryOptions({ term: 'widget' }));

Appliquez le trait @mutation à votre opération Smithy pour la forcer à être traitée comme une 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());
startProcessing.mutate({ id: 'some-id' });

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

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

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

infiniteQueryOptions pagine alors sur nextToken :

const items = useInfiniteQuery({
...api.listPagedItems.infiniteQueryOptions(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextToken || undefined },
),
});

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

@http(method: "GET", uri: "/unpaged-items")
@cursor(enabled: false)
operation ListUnpagedItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
@httpQuery("cursor")
cursor: String
}
output := {
@required
items: ItemList
}
}

api.listUnpagedItems ne fournit alors que queryOptions, queryKey et queryFilter.

Les hooks et méthodes client générés sont automatiquement organisés en fonction du trait @tags dans vos opérations Smithy. Les opérations avec les mêmes tags sont regroupées, ce qui aide à garder vos appels d’API organisés et fournit une meilleure complétion de code dans votre IDE.

Au sein d’un groupe, chaque opération conserve son propre nom en camelCase, donc une opération nommée ListItems taguée "items" est accessible à api.items.listItems — pas api.items.list.

Par exemple, avec ce modèle Smithy :

service MyService {
operations: [ListItems, CreateItem, ListUsers, CreateUser]
}
@tags(["items"])
@http(method: "GET", uri: "/items")
@readonly
operation ListItems {
input := {}
output := {
@required
items: ItemList
}
}
@tags(["items"])
@http(method: "POST", uri: "/items")
operation CreateItem {
input := {
@required
name: String
}
output := {
@required
id: String
}
}
@tags(["users"])
@http(method: "GET", uri: "/users")
@readonly
operation ListUsers {
input := {}
output := {
@required
users: UserList
}
}
@tags(["users"])
@http(method: "POST", uri: "/users")
operation CreateUser {
input := {
@required
name: String
}
output := {
@required
id: String
}
}

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();
// Items operations are grouped under api.items
const items = useQuery(api.items.listItems.queryOptions());
const createItem = useMutation(api.items.createItem.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.listUsers.queryOptions());
// Usage example
const handleCreateItem = () => {
createItem.mutate({ name: 'New Item' });
};
return (
<div>
<h2>Items</h2>
<ul>
{items.data?.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2>
<ul>
{users.data?.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

Ce regroupement facilite l’organisation de vos appels d’API et fournit une meilleure complétion de code dans votre IDE.

Cliquez ici pour un exemple utilisant directement le client.

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éfinition de structures d’erreur personnalisées

Section intitulée « Définition de structures d’erreur personnalisées »

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

@error("client")
@httpError(422)
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
}

Utilisation de types d’erreur personnalisés dans React

Section intitulée « Utilisation de 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 le type et de gérer différentes réponses d’erreur :

import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() {
const api = useMyApi();
const getItem = useQuery(api.getItem.queryOptions({ itemId: '123' }));
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
switch (getItem.error.status) {
case 404:
// error.error is typed as ItemNotFoundErrorResponseContent
return <NotFoundMessage message={getItem.error.error.message} />;
case 500:
// error.error is typed as InternalServerErrorResponseContent
return <ErrorMessage message={getItem.error.error.message} />;
}
}
return (
<div>
{/* Component content */}
</div>
);
}
Cliquez ici pour un exemple utilisant directement le client.

Gérez toujours 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 is typed as UnauthorizedErrorResponseContent
return <ErrorMessage message={err.error.reason} />;
case 500:
// err.error is typed as InternalServerErrorResponseContent
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data?.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

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

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ListItemsResponseContent } from '../generated/my-api/types.gen';
function ItemList() {
const api = useMyApi();
const queryClient = useQueryClient();
// Query to fetch items
const itemsQuery = useQuery(api.listItems.queryOptions({}));
// Mutation for deleting items with optimistic updates
const deleteMutation = useMutation({
...api.deleteItem.mutationOptions(),
onMutate: async ({ itemId }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: api.listItems.queryKey({}),
});
// Snapshot the previous value
const previousItems = queryClient.getQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
);
// Optimistically update to the new value
queryClient.setQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
(old) =>
old && {
...old,
items: old.items.filter((item) => item.id !== itemId),
},
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, _input, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(
api.listItems.queryKey({}),
context?.previousItems,
);
console.error('Failed to delete item:', err);
},
onSettled: () => {
// Always refetch after error or success to ensure data is in sync with server
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
},
});
if (itemsQuery.isLoading) {
return <LoadingSpinner />;
}
if (itemsQuery.isError) {
return <ErrorMessage message="Failed to load items" />;
}
return (
<ul>
{itemsQuery.data?.items.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate({ itemId: item.id })}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

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

import { useMutation } from '@tanstack/react-query';
function ItemForm() {
const api = useMyApi();
// Type-safe mutation for creating items
const createItem = useMutation({
...api.createItem.mutationOptions(),
// ✅ Type error if onSuccess callback doesn't handle the correct response type
onSuccess: (data) => {
// data is fully typed based on your API's response schema
console.log(`Item created with ID: ${data.id}`);
},
});
const handleSubmit = (data: CreateItemInput) => {
// ✅ Type error if input doesn't match schema
createItem.mutate(data);
};
// Error UI can use type narrowing to handle different error types
if (createItem.error) {
const error = createItem.error;
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerErrorResponseContent for 500, etc.
return <ServerError message={error.error.message} />;
}
}
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({ name: 'New Item' });
}}>
{/* Form fields */}
<button
type="submit"
disabled={createItem.isPending}
>
{createItem.isPending ? 'Creating...' : 'Create Item'}
</button>
</form>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

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 une compilation.

Si votre API Smithy utilise l’authentification Custom (Lambda Authorizer), vous devrez modifier le provider de client généré pour ajouter les en-têtes d’autorisation que votre authorizer attend. Recherchez la configuration fetch dans le <ApiName>Provider.tsx généré et ajoutez votre jeton ou clé API aux en-têtes de requête.