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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParametroTipoPredefinitoDescrizione
sourceProject Obbligatoriostring-Il progetto sorgente
targetProject Obbligatoriostring-Il progetto di destinazione a cui connettersi
sourceComponent string-Il componente sorgente da cui connettersi (nome del componente, percorso relativo alla radice del progetto sorgente, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come sorgente.
targetComponent string-Il componente di destinazione a cui connettersi (nome del componente, percorso relativo alla radice del progetto di destinazione, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come destinazione.
preferInstallDependencies booleantrueSe 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.isLoading) return <div>Loading...</div>;
if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;
}
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: {createItem.error.message}
</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.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>
);
}

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 <operation-name>Error che incapsula le possibili risposte di errore definite nel modello Smithy. Ogni errore ha una proprietà status e error, e controllando il valore di status puoi restringere a un tipo specifico di errore.

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

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

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

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

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

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

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: "/items")
@cursor(inputToken: "nextToken")
operation ListItems {
input := {
nextToken: String
limit: Integer
}
output := {
items: ItemList
nextToken: String
}
}

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

@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 := {
...
}
}

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.

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

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

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(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
}

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();
// 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>
);
}
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 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>
);
}
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';
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>
);
}
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 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>
);
}
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.