Salta ai contenuti

Reagire all'API Smithy

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

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

  1. Un file main.tsx che renderizza l’applicazione
  2. Un backend API Smithy TypeScript funzionante (generato usando il generatore ts#smithy-api)
  3. Autenticazione Cognito aggiunta tramite il generatore ts#react-website-auth se si connette un’API che utilizza autenticazione Cognito o IAM
Esempio della struttura richiesta per 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>,
);
  1. Installa il Nx Console VSCode Plugin se non l'hai già fatto
  2. Apri la console Nx in VSCode
  3. Clicca su Generate (UI) nella sezione "Common Nx Commands"
  4. Cerca @aws/nx-plugin - connection
  5. Compila i parametri richiesti
    • Clicca su Generate
    Parametro Tipo Predefinito Descrizione
    sourceProject Obbligatorio string - The source project
    targetProject Obbligatorio string - The target project to connect to

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

    • Directorysrc
      • Directorycomponents
        • <ApiName>Provider.tsx Provider per il 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 l’API con 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 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 generato sono ignorati per impostazione predefinita

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

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

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

    Durante la build, viene generato un client type-safe dalla specifica OpenAPI dell’API Smithy. 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 l’API
          • options-proxy.gen.ts Fornisce metodi per creare opzioni degli hook TanStack Query per interagire con l’API usando TanStack Query

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

    Il generatore fornisce un hook use<ApiName> che puoi usare per chiamare l’API con TanStack Query.

    Puoi usare il metodo queryOptions per recuperare le opzioni necessarie per chiamare l’API usando 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 usando l’hook useMutation di TanStack Query. Questo fornisce un modo pulito per gestire 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();
    // Crea una mutation usando le opzioni di mutation generate
    const createItem = useMutation(api.createItem.mutationOptions());
    const handleSubmit = (e) => {
    e.preventDefault();
    createItem.mutate({ name: 'New Item', description: 'A new item' });
    };
    return (
    <form onSubmit={handleSubmit}>
    {/* Campi del form */}
    <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) => {
    // Questo verrà eseguito quando la mutation ha successo
    console.log('Item created:', data);
    // Puoi navigare al nuovo item
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Questo verrà eseguito quando la mutation fallisce
    console.error('Failed to create item:', error);
    },
    onSettled: () => {
    // Questo verrà eseguito quando la mutation si completa (successo o errore)
    // Buon posto per invalidare le query che potrebbero essere interessate
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Clicca qui per un esempio usando direttamente il client.

    Per gli endpoint che accettano un parametro cursor come input, gli hook generati forniscono supporto per le infinite queries usando l’hook useInfiniteQuery di TanStack Query. Questo rende facile implementare la funzionalità “carica altro” o scroll 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, // Numero di elementi per pagina
    }, {
    // Assicurati di definire una funzione getNextPageParam per restituire
    // il parametro che dovrebbe essere passato come 'cursor' per la
    // prossima pagina
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    {/* Appiattisci l'array delle pagine per renderizzare tutti gli elementi */}
    <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 usando 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 è tipizzato come CreateItem400Response
    return (
    <div>
    <h2>Invalid input:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    case 403:
    // error.error è tipizzato come CreateItem403Response
    return (
    <div>
    <h2>Not authorized:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error è tipizzato come 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 usando direttamente il client vanilla.

    Una selezione di trait Smithy viene aggiunta al tuo progetto model Smithy di destinazione in extensions.smithy che puoi usare 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 usando 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: "/items")
    @query
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }

    L’hook generato fornirà queryOptions anche se usa 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 usa 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 usando il trait @cursor che viene aggiunto al tuo progetto model in extensions.smithy.

    Applica il trait @cursor con inputToken per cambiare 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 := {
    // Il parametro di input chiamato 'cursor' farà sì che questa operazione venga trattata come un'operazione paginata per impostazione predefinita
    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();
    // Le operazioni Items sono raggruppate sotto api.items
    const items = useQuery(api.items.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // Le operazioni Users sono raggruppate sotto api.users
    const users = useQuery(api.users.listUsers.queryOptions());
    // Esempio di utilizzo
    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 usando 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 le tue operazioni possono restituire:

    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, permettendoti di verificare i tipi e gestire diverse risposte di errore:

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // Query con gestione degli errori tipizzata
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // L'errore è tipizzato in base agli errori nel tuo modello Smithy
    switch (error.status) {
    case 404:
    // error.error è tipizzato come ItemNotFoundError
    console.error('Not found:', error.error.message);
    break;
    case 500:
    // error.error è tipizzato come InternalServerError
    console.error('Server error:', error.error.message);
    console.error('Trace ID:', error.error.traceId);
    break;
    }
    }
    });
    // Mutation con gestione degli errori tipizzata
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error è tipizzato come InvalidRequestError
    console.error('Validation error:', error.error.message);
    console.error('Field errors:', error.error.fieldErrors);
    break;
    case 403:
    // error.error è tipizzato come UnauthorizedError
    console.error('Unauthorized:', error.error.reason);
    break;
    }
    }
    });
    // Rendering del componente con gestione degli errori
    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>
    {/* Contenuto del componente */}
    </div>
    );
    }
    Clicca qui per un esempio usando 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 è tipizzato come ListItems403Response
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error è tipizzato come 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 usando 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 per recuperare gli items
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // Mutation per eliminare items con aggiornamenti ottimistici
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // Annulla eventuali refetch in corso
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Snapshot del valore precedente
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Aggiorna ottimisticamente al nuovo valore
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Restituisci un oggetto di contesto con lo snapshot
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // Se la mutation fallisce, usa il contesto restituito da onMutate per ripristinare
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Failed to delete item:', err);
    },
    onSettled: () => {
    // Refetch sempre dopo errore o successo per assicurare che i dati siano sincronizzati con il 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 usando 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();
    // Mutation type-safe per creare items
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ Errore di tipo se il callback onSuccess non gestisce il tipo di risposta corretto
    onSuccess: (data) => {
    // data è completamente tipizzato in base allo schema di risposta della tua API
    console.log(`Item created with ID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    // ✅ Errore di tipo se l'input non corrisponde allo schema
    createItem.mutate(data);
    };
    // L'UI degli errori può usare il type narrowing per gestire diversi tipi di errore
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error è tipizzato come InvalidRequestError
    return (
    <FormError
    message="Invalid input"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error è tipizzato come UnauthorizedError
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error è tipizzato come InternalServerError per 500, ecc.
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: 'New Item' });
    }}>
    {/* Campi del form */}
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? 'Creating...' : 'Create Item'}
    </button>
    </form>
    );
    }
    Clicca qui per un esempio usando 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.