Salta ai contenuti

React a Smithy API

Il generatore connection fornisce un modo per integrare rapidamente il tuo sito web React con il tuo backend Smithy TypeScript API. Configura tutta la configurazione necessaria per connettersi alla tua Smithy API in modo type-safe, inclusa la generazione di client e hook TanStack Query, il supporto per l’autenticazione AWS IAM e Cognito e la corretta gestione degli errori.

Prima di utilizzare questo generatore, assicurati che la tua applicazione React abbia:

  1. Un file main.tsx che renderizza la tua applicazione
  2. Un backend Smithy TypeScript API funzionante (generato utilizzando il generatore ts#api con --framework=smithy)
  3. Cognito Auth aggiunto tramite il generatore ts#website#auth se si connette un’API che utilizza l’autenticazione Cognito o IAM
Esempio di struttura main.tsx richiesta
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>,
);

Esegui questo generatore@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
Componi il tuo comando5

Obbligatorio

Obbligatorio

Opzioni del generatore5 opzioni
sourceProjectObbligatoriostring

Il progetto sorgente

targetProjectObbligatoriostring

Il progetto di destinazione a cui connettersi

sourceComponentstring

Il componente sorgente da cui connettersi (nome del componente, percorso relativo alla radice del progetto sorgente, o id del generatore). Usa '.' per selezionare esplicitamente il progetto come sorgente.

targetComponentstring

Il componente destinazione a cui connettersi (nome del componente, percorso relativo alla radice del progetto destinazione, o id del generatore). Usa '.' per selezionare esplicitamente il progetto come destinazione.

preferInstallDependenciesbooleanPredefinito: true

Se preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine.

Il generatore apporterà modifiche ai seguenti file nella tua applicazione React:

  • Directorysrc
    • Directorycomponents
      • <ApiName>Provider.tsx Provider per il tuo client API
      • QueryClientProvider.tsx Provider del client TanStack React Query
      • DirectoryRuntimeConfig/ Componente di configurazione runtime per lo sviluppo locale
    • Directoryhooks
      • use<ApiName>.tsx Aggiunge un hook per chiamare la tua API con lo stato gestito da TanStack Query
      • use<ApiName>Client.tsx Aggiunge un hook per istanziare il client API vanilla che può chiamare la tua API.
      • useSigV4.tsx Aggiunge un hook per firmare le richieste HTTP con SigV4 (se hai selezionato l’autenticazione IAM)
  • project.json Un nuovo target viene aggiunto alla build che genera un client type-safe
  • .gitignore I file del client generati sono ignorati per impostazione predefinita

Il generatore aggiungerà anche un file al tuo modello Smithy:

  • Directorymodel
    • Directorysrc
      • extensions.smithy Definisce i trait che possono essere utilizzati per personalizzare il client generato

Il generatore aggiungerà anche Runtime Config all’infrastruttura del tuo sito web se non è già presente, il che garantisce che l’URL dell’API per la tua Smithy API sia disponibile nel sito web e configurato automaticamente dall’hook use<ApiName>.tsx.

Al momento della build, un client type-safe viene generato dalla specifica OpenAPI della tua Smithy API. Questo aggiungerà tre nuovi file alla tua applicazione React:

  • Directorysrc
    • Directorygenerated
      • Directory<ApiName>
        • types.gen.ts Tipi generati dalle strutture del modello Smithy
        • client.gen.ts Client type-safe per chiamare la tua API
        • options-proxy.gen.ts Fornisce metodi per creare opzioni di hook TanStack Query per interagire con la tua API utilizzando TanStack Query

Il client type-safe generato può essere utilizzato per chiamare la tua Smithy API dalla tua applicazione React. Si consiglia di utilizzare il client tramite gli hook TanStack Query, ma puoi utilizzare il client vanilla se preferisci.

Il generatore fornisce un hook use<ApiName> che puoi utilizzare per chiamare la tua API con TanStack Query.

Puoi utilizzare il metodo queryOptions per recuperare le opzioni necessarie per chiamare la tua API utilizzando l’hook useQuery di 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’errore è un’unione discriminata di oggetti { status, error } piuttosto che un Error, quindi non c’è un message di livello superiore da renderizzare. Restringi su status per leggere un corpo di risposta specifico — vedi Gestione degli Errori di seguito.

Clicca qui per un esempio usando direttamente il client vanilla.

Gli hook generati includono il supporto per le mutazioni utilizzando l’hook useMutation di TanStack Query. Questo fornisce un modo pulito per gestire le operazioni di creazione, aggiornamento ed eliminazione con stati di caricamento, gestione degli errori e aggiornamenti ottimistici.

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>
);
}

Puoi anche aggiungere callback per diversi stati della 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({}) });
}
});
Clicca qui per un esempio utilizzando direttamente il client.

Per gli endpoint che accettano un parametro cursor come input, gli hook generati forniscono supporto per query infinite utilizzando l’hook useInfiniteQuery di TanStack Query. Questo rende facile implementare la funzionalità “carica altro” o lo scorrimento infinito.

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>
);
}

Gli hook generati gestiscono automaticamente la paginazione basata su cursore se la tua API la supporta. Il valore nextCursor viene estratto dalla risposta e utilizzato per recuperare la pagina successiva.

Clicca qui per un esempio utilizzando direttamente il client.

L’integrazione include una gestione degli errori integrata con risposte di errore tipizzate. Viene generato un tipo <OperationName>Error che è un’unione di un membro <OperationName><StatusCode>Error per ogni stato di errore che l’operazione modella. Ogni membro ha una proprietà status e una proprietà error, quindi lo switch su status restringe error al payload di quello stato.

L’esempio seguente presuppone che CreateItem modelli le strutture di errore definite più avanti in questa pagina — un InvalidRequestError 422, un UnauthorizedError 403 e un InternalServerError 500. Solo gli stati dichiarati dal tuo modello appaiono nell’unione, quindi un case per uno stato che non hai modellato è un errore di compilazione.

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>;
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

Una selezione di trait Smithy viene aggiunta al tuo progetto Smithy model di destinazione in extensions.smithy che puoi utilizzare per personalizzare il client generato.

Per impostazione predefinita, le operazioni nella tua API Smithy che utilizzano i metodi HTTP PUT, POST, PATCH e DELETE sono considerate mutazioni, e tutte le altre sono considerate query.

Puoi modificare questo comportamento utilizzando i trait Smithy @query e @mutation che vengono aggiunti al tuo progetto model in extensions.smithy.

Applica il trait @query alla tua operazione Smithy per forzarla a essere trattata come una query:

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

L’hook generato fornirà queryOptions anche se utilizza il metodo HTTP POST:

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

Applica il trait @mutation alla tua operazione Smithy per forzarla a essere trattata come una mutation:

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

L’hook generato fornirà mutationOptions anche se utilizza il metodo HTTP GET:

const startProcessing = useMutation(api.startProcessing.mutationOptions());
startProcessing.mutate({ id: 'some-id' });

Per impostazione predefinita, gli hook generati assumono una paginazione basata su cursore con un parametro chiamato cursor. Puoi personalizzare questo comportamento utilizzando il trait @cursor che viene aggiunto al tuo progetto model in extensions.smithy.

Applica il trait @cursor con inputToken per modificare il nome del parametro di input utilizzato per il token di paginazione:

@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 quindi pagina su nextToken:

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

Se non desideri generare infiniteQueryOptions per un’operazione che ha un parametro di input chiamato cursor, puoi disabilitare la paginazione basata su cursore:

@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 quindi fornisce solo queryOptions, queryKey e queryFilter.

Gli hook generati e i metodi del client sono organizzati automaticamente in base al trait @tags nelle tue operazioni Smithy. Le operazioni con gli stessi tag vengono raggruppate insieme, il che aiuta a mantenere organizzate le chiamate API e fornisce un migliore completamento del codice nel tuo IDE.

All’interno di un gruppo ogni operazione mantiene il proprio nome in camelCase, quindi un’operazione chiamata ListItems taggata "items" si raggiunge a api.items.listItems — non api.items.list.

Ad esempio, con questo modello 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
}
}

Gli hook generati saranno raggruppati per tag:

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>
);
}

Questo raggruppamento rende più facile organizzare le chiamate API e fornisce un migliore completamento del codice nel tuo IDE.

Clicca qui per un esempio utilizzando direttamente il client.

Puoi personalizzare le risposte di errore nella tua API Smithy definendo strutture di errore personalizzate nel tuo modello Smithy. Il client generato gestirà automaticamente questi tipi di errore personalizzati.

Definisci le tue strutture di errore nel tuo modello 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
}

Specifica quali errori possono restituire le tue operazioni:

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
}

Utilizzo di Tipi di Errore Personalizzati in React

Sezione intitolata “Utilizzo di Tipi di Errore Personalizzati in React”

Il client generato gestirà automaticamente questi tipi di errore personalizzati, consentendoti di controllare il tipo e gestire diverse risposte di errore:

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>
);
}
Clicca qui per un esempio utilizzando direttamente il client.

Gestisci sempre gli stati di caricamento ed errore per una migliore esperienza utente:

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>
);
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

Implementa aggiornamenti ottimistici per una migliore esperienza utente:

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>
);
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

L’integrazione fornisce type safety completa end-to-end. Il tuo IDE fornirà autocompletamento completo e controllo dei tipi per tutte le chiamate 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>
);
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

I tipi sono generati automaticamente dallo schema OpenAPI della tua API Smithy, assicurando che qualsiasi modifica alla tua API si rifletta nel codice frontend dopo una build.

Se la tua API Smithy utilizza l’autenticazione Custom (Lambda Authorizer), dovrai modificare il provider del client generato per aggiungere gli header di autorizzazione che il tuo authorizer si aspetta. Cerca la configurazione fetch nel file generato <ApiName>Provider.tsx e aggiungi il tuo token o chiave API agli header della richiesta.