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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParamètreTypePar défautDescription
sourceProject Requisstring-Le projet source
targetProject Requisstring-Le projet cible auquel se connecter
sourceComponent string-Le composant source depuis lequel se connecter (nom du composant, chemin relatif à la racine du projet source, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme source.
targetComponent string-Le composant cible auquel se connecter (nom du composant, chemin relatif à la racine du projet cible, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme cible.
preferInstallDependencies booleantrueIndique 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.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 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: {createItem.error.message}
</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.isLoading) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={items.error.message} />;
}
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 <operation-name>Error est généré qui encapsule les réponses d’erreur possibles définies dans le modèle Smithy. Chaque erreur a une propriété status et error, et en vérifiant la valeur de status, vous pouvez réduire à 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 is typed as CreateItem400Response
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as CreateItem403Response
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
case 502:
// error.error is typed as CreateItem5XXResponse
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 ensuite le trait @query à votre opération Smithy pour la forcer à être traitée comme une 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 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());

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: "/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 qui a un paramètre d’entrée nommé cursor, vous pouvez désactiver la pagination basée sur le curseur :

@cursor(enabled: false)
operation ListItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
cursor: String
}
output := {
...
}
}

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.

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();
// 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?.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</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 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(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
}

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();
// Query with typed error handling
const getItem = useQuery({
...api.getItem.queryOptions({ itemId: '123' }),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 404:
// error.error is typed as ItemNotFoundError
console.error('Not found:', error.error.message);
break;
case 500:
// error.error is typed as InternalServerError
console.error('Server error:', error.error.message);
console.error('Trace ID:', error.error.traceId);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as InvalidRequestError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedError
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
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>
{/* 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 ListItems403Response
return <ErrorMessage message={err.error.reason} />;
case 500:
case 502:
// err.error is typed as ListItems5XXResponse
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data.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';
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(api.listItems.queryKey());
// Optimistically update to the new value
queryClient.setQueryData(
api.listItems.queryKey(),
(old) => old.filter((item) => item.id !== itemId)
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, itemId, 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.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate(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 400:
// error.error is typed as InvalidRequestError
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedError
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerError 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.