Salta ai contenuti

React to FastAPI

Il generatore api-connection fornisce un modo rapido per integrare il tuo sito React con il backend FastAPI. Configura tutto il necessario per connettersi ai backend FastAPI in modo type-safe, includendo la generazione di client e hook per TanStack Query, il supporto per autenticazione AWS IAM e Cognito, e una corretta gestione degli errori.

Prerequisiti

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

  1. Un file main.tsx che renderizza l’applicazione
  2. Un backend FastAPI funzionante (generato usando il generatore FastAPI)
  3. Cognito Auth aggiunto tramite il generatore ts#cloudscape-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>,
);

Utilizzo

Esegui il Generatore

  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 - api-connection
  5. Compila i parametri richiesti
    • Clicca su Generate

    Opzioni

    Parametro Tipo Predefinito Descrizione
    sourceProject Obbligatorio string - The source project which will call the API
    targetProject Obbligatorio string - The target project containing your API

    Output del Generatore

    Il generatore modificherà i seguenti file nel tuo progetto FastAPI:

    • Directoryscripts
      • generate_open_api.py Aggiunge uno script che genera una specifica OpenAPI per la tua API
    • project.json Aggiunge un nuovo target alla build che invoca lo script di generazione

    Il generatore modificherà i seguenti file nella tua applicazione React:

    • Directorysrc
      • Directorycomponents
        • <ApiName>Provider.tsx Provider per il client della tua API
        • QueryClientProvider.tsx Provider del client TanStack React Query
      • 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 vanilla che può chiamare la tua API
        • useSigV4.tsx Aggiunge un hook per firmare le richieste HTTP con SigV4 (se è stata selezionata l’autenticazione IAM)
    • project.json Aggiunge un nuovo target alla build che genera un client type-safe
    • .gitignore I file del client generato vengono ignorati di default

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

    Generazione del Codice

    Durante la build, viene generato un client type-safe dalla specifica OpenAPI del FastAPI. Questo aggiungerà tre nuovi file alla tua applicazione React:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Tipi generati dai modelli pydantic definiti nel FastAPI
          • 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

    Utilizzo del Codice Generato

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

    Utilizzo dell’Hook API

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

    Query

    Puoi usare il metodo queryOptions per ottenere le opzioni richieste per chiamare la tua 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 che usa direttamente il client vanilla.

    Mutazioni

    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 e cancellazione con stati di caricamento, gestione errori e aggiornamenti ottimistici.

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    // Crea una mutazione usando le opzioni 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 mutazione:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    // Eseguito quando la mutazione ha successo
    console.log('Item created:', data);
    // Puoi navigare verso il nuovo item
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Eseguito quando la mutazione fallisce
    console.error('Failed to create item:', error);
    },
    onSettled: () => {
    // Eseguito al completamento della mutazione (successo o errore)
    // Buon punto per invalidare query che potrebbero essere affette
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Clicca qui per un esempio che usa direttamente il client.

    Paginazione con Infinite Queries

    Per endpoint che accettano un parametro cursor come input, gli hook generati forniscono supporto per infinite queries usando l’hook useInfiniteQuery di TanStack Query. Questo semplifica l’implementazione di funzionalità “carica più” 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 item per pagina
    }, {
    // Assicurati di definire una funzione getNextPageParam che restituisce
    // il parametro da passare come 'cursor' per la pagina successiva
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    {/* Appiattisci l'array pages per renderizzare tutti gli item */}
    <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 cursor se la tua API la supporta. Il valore nextCursor viene estratto dalla risposta e usato per recuperare la pagina successiva.

    Clicca qui per un esempio che usa direttamente il client.

    Gestione degli Errori

    L’integrazione include una gestione errori integrata con risposte di errore tipizzate. Viene generato un tipo <operation-name>Error che incapsula le possibili risposte di errore definite nella specifica OpenAPI. 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>
    <ul>
    {createItem.error.error.validationErrors.map((err) => (
    <li key={err.field}>{err.message}</li>
    ))}
    </ul>
    </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>
    <p>Trace ID: {createItem.error.error.traceId}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Create Item</button>;
    }
    Clicca qui per un esempio che usa direttamente il client vanilla.

    Consumo di uno Stream

    Se hai configurato il FastAPI per streammare risposte, il tuo hook useQuery aggiornerà automaticamente i dati man mano che arrivano nuovi chunk dello stream.

    Ad esempio:

    function MyStreamingComponent() {
    const api = useMyApi();
    const stream = useQuery(api.myStream.queryOptions());
    return (
    <ul>
    {(stream.data ?? []).map((chunk) => (
    <li>
    {chunk.timestamp.toISOString()}: {chunk.message}
    </li>
    ))}
    </ul>
    );
    }

    Puoi usare le proprietà isLoading e fetchStatus per determinare lo stato corrente dello stream se necessario. Uno stream segue questo ciclo di vita:

    1. Viene inviata la richiesta HTTP per avviare lo streaming

      • isLoading è true
      • fetchStatus è 'fetching'
      • data è undefined
    2. Viene ricevuto il primo chunk dello stream

      • isLoading diventa false
      • fetchStatus rimane 'fetching'
      • data diventa un array contenente il primo chunk
    3. Vengono ricevuti chunk successivi

      • isLoading rimane false
      • fetchStatus rimane 'fetching'
      • data viene aggiornato con ogni chunk successivo appena ricevuto
    4. Lo stream si completa

      • isLoading rimane false
      • fetchStatus diventa 'idle'
      • data è un array di tutti i chunk ricevuti
    Clicca qui per un esempio che usa direttamente il client vanilla.

    Personalizzazione del Codice Generato

    Query e Mutazioni

    Di default, le operazioni nel FastAPI che usano i metodi HTTP PUT, POST, PATCH e DELETE sono considerate mutazioni, tutte le altre sono considerate query.

    Puoi modificare questo comportamento usando x-query e x-mutation.

    x-query

    @app.post(
    "/items",
    openapi_extra={
    "x-query": True
    }
    )
    def list_items():
    # ...

    L’hook generato fornirà queryOptions nonostante usi il metodo HTTP POST:

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

    x-mutation

    @app.get(
    "/start-processing",
    openapi_extra={
    "x-mutation": True
    }
    )
    def start_processing():
    # ...

    L’hook generato fornirà mutationOptions nonostante usi il metodo HTTP GET:

    // L'hook generato includerà le opzioni personalizzate
    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Cursor di Paginazione Personalizzato

    Di default, gli hook generati assumono una paginazione basata su cursor con un parametro chiamato cursor. Puoi personalizzare questo comportamento usando l’estensione x-cursor:

    @app.get(
    "/items",
    openapi_extra={
    # Specifica un nome diverso per il parametro del cursor
    "x-cursor": "page_token"
    }
    )
    def list_items(page_token: str = None, limit: int = 10):
    # ...
    return {
    "items": items,
    "page_token": next_page_token # La risposta deve includere il cursor con lo stesso nome
    }

    Se non vuoi generare infiniteQueryOptions per un’operazione, puoi impostare x-cursor a False:

    @app.get(
    "/items",
    openapi_extra={
    # Disabilita la paginazione basata su cursor per questo endpoint
    "x-cursor": False
    }
    )
    def list_items(page: int = 1, limit: int = 10):
    # ...
    return {
    "items": items,
    "total": total_count,
    "page": page,
    "pages": total_pages
    }

    Raggruppamento Operazioni

    Gli hook e i metodi del client generati sono organizzati automaticamente in base ai tag OpenAPI negli endpoint del FastAPI. Questo aiuta a mantenere organizzate le chiamate API e facilita la ricerca di operazioni correlate.

    Ad esempio:

    items.py
    @app.get(
    "/items",
    tags=["items"],
    )
    def list():
    # ...
    @app.post(
    "/items",
    tags=["items"],
    )
    def create(item: Item):
    # ...
    users.py
    @app.get(
    "/users",
    tags=["users"],
    )
    def list():
    # ...

    Gli hook generati saranno raggruppati per questi 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.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Le operazioni users sono raggruppate sotto api.users
    const users = useQuery(api.users.list.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 semplifica l’organizzazione delle chiamate API e fornisce un migliore code completion nel tuo IDE.

    Clicca qui per un esempio che usa direttamente il client.

    Errori

    Puoi personalizzare le risposte di errore nel FastAPI definendo classi di eccezioni personalizzate, gestori di eccezioni e specificando modelli di risposta per diversi codici di stato. Il client generato gestirà automaticamente questi tipi di errore personalizzati.

    Definizione Modelli di Errore Personalizzati

    Prima definisci i modelli di errore usando Pydantic:

    models.py
    from pydantic import BaseModel
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    Creazione Eccezioni Personalizzate

    Poi crea classi di eccezione per diversi scenari di errore:

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    Aggiunta Gestori di Eccezione

    Registra i gestori di eccezione per convertire le tue eccezioni in risposte HTTP:

    main.py
    from fastapi import Request
    from fastapi.responses import JSONResponse
    @app.exception_handler(NotFoundException)
    async def not_found_handler(request: Request, exc: NotFoundException):
    return JSONResponse(
    status_code=404,
    content=exc.message,
    )
    @app.exception_handler(ValidationException)
    async def validation_error_handler(request: Request, exc: ValidationException):
    return JSONResponse(
    status_code=400,
    content=exc.details.model_dump(),
    )

    Specifica Modelli di Risposta

    Infine, specifica i modelli di risposta per diversi codici di stato nelle definizioni degli endpoint:

    main.py
    @app.get(
    "/items/{item_id}",
    responses={
    404: {"model": str}
    500: {"model": ErrorDetails}
    }
    )
    def get_item(item_id: str) -> Item:
    item = find_item(item_id)
    if not item:
    raise NotFoundException(message=f"Item with ID {item_id} not found")
    return item
    @app.post(
    "/items",
    responses={
    400: {"model": ValidationError},
    403: {"model": str}
    }
    )
    def create_item(item: Item) -> Item:
    if not is_valid(item):
    raise ValidationException(
    ValidationError(
    message="Invalid item data",
    field_errors=["name is required"]
    )
    )
    return save_item(item)

    Utilizzo Tipi di Errore Personalizzati in React

    Il client generato gestirà automaticamente questi tipi di errore personalizzati, permettendoti di fare type-check e gestire diverse risposte di errore:

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // Query con gestione errori tipizzata
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // L'errore è tipizzato in base alle risposte nel FastAPI
    switch (error.status) {
    case 404:
    // error.error è una stringa come specificato nelle risposte
    console.error('Not found:', error.error);
    break;
    case 500:
    // error.error è tipizzato come ErrorDetails
    console.error('Server error:', error.error.message);
    break;
    }
    }
    });
    // Mutazione con gestione errori tipizzata
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error è tipizzato come ValidationError
    console.error('Validation error:', error.error.message);
    console.error('Field errors:', error.error.field_errors);
    break;
    case 403:
    // error.error è una stringa come specificato nelle risposte
    console.error('Forbidden:', error.error);
    break;
    }
    }
    });
    // Render del componente con gestione errori
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error} />;
    } else {
    return <ErrorMessage message={getItem.error.error.message} />;
    }
    }
    return (
    <div>
    {/* Contenuto del componente */}
    </div>
    );
    }
    Clicca qui per un esempio che usa direttamente il client.

    Best Practices

    Gestione Stati di Caricamento

    Gestisci sempre gli stati di caricamento e errori 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}
    details={`Trace ID: ${err.error.traceId}`}
    />
    );
    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 che usa direttamente il client vanilla.

    Aggiornamenti Ottimistici

    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 item
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // Mutazione per eliminare item 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());
    // Aggiornamento ottimistico al nuovo valore
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Restituisci un oggetto contesto con lo snapshot
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // Se la mutazione fallisce, usa il contesto per ripristinare
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Failed to delete item:', err);
    },
    onSettled: () => {
    // Rifà sempre il fetch dopo errore o successo per sincronizzazione
    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 che usa direttamente il client vanilla.

    Type Safety

    L’integrazione fornisce type safety end-to-end completa. Il tuo IDE fornirà autocompletamento e type checking per tutte le chiamate API:

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // Mutazione type-safe per creare item
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ Errore di tipo se la callback onSuccess non gestisce il tipo di risposta corretto
    onSuccess: (data) => {
    // data è completamente tipizzato in base allo schema di risposta dell'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);
    };
    // UI errori può usare type narrowing per gestire diversi tipi di errore
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error è tipizzato come CreateItem400Response
    return (
    <FormError
    message="Invalid input"
    errors={error.error.validationErrors}
    />
    );
    case 403:
    // error.error è tipizzato come CreateItem403Response
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error è tipizzato come CreateItem5XXResponse per 500, 502, 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 che usa direttamente il client vanilla.

    I tipi sono generati automaticamente dallo schema OpenAPI del FastAPI, assicurando che qualsiasi modifica all’API si rifletta nel codice frontend dopo una build.