Salta ai contenuti

React con 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, inclusa la generazione di client e hook TanStack Query, il supporto per l’autenticazione AWS IAM e una corretta gestione degli errori.

Prerequisiti

Prima di usare 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)
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
    auth string IAM Authentication strategy (choose from IAM or None)

    Output del Generatore

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

    • Directoryscripts
      • generate_open_api.py Aggiunge uno script per generare la specifica OpenAPI dell’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 API
        • QueryClientProvider.tsx Provider del client TanStack React Query
      • 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
        • 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 generati del client vengono ignorati per default

    Il generatore aggiungerà anche Runtime Config all’infrastruttura del tuo sito web se non presente, garantendo che l’URL dell’API 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 di FastAPI. Questo aggiungerà tre nuovi file all’applicazione React:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Tipi generati dai modelli pydantic definiti in FastAPI
          • client.gen.ts Client type-safe per chiamare l’API
          • options-proxy.gen.ts Fornisce metodi per creare opzioni degli hook TanStack Query

    Utilizzo del Codice Generato

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

    Utilizzo dell’Hook API

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

    Query

    Puoi usare il metodo queryOptions per ottenere le opzioni necessarie per chiamare l’API con 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>;
    }
    Fai clic qui per un esempio che utilizza direttamente il client vanilla.

    Mutazioni

    Gli hook generati supportano le mutazioni con l’hook useMutation di TanStack Query, offrendo 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 ? 'Creazione...' : 'Crea Item'}
    </button>
    {createItem.isSuccess && (
    <div className="success">
    Item creato con ID: {createItem.data.id}
    </div>
    )}
    {createItem.isError && (
    <div className="error">
    Errore: {createItem.error.message}
    </div>
    )}
    </form>
    );
    }

    Puoi aggiungere callback per diversi stati della mutazione:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    // Eseguito quando la mutazione ha successo
    console.log('Item creato:', data);
    // Naviga verso il nuovo item
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // Eseguito in caso di errore
    console.error('Creazione fallita:', error);
    },
    onSettled: () => {
    // Eseguito al completamento (successo o errore)
    // Luogo ideale per invalidare query correlate
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Fai clic qui per un esempio che utilizza il client direttamente.

    Paginazione con Infinite Query

    Per endpoint che accettano un parametro cursor, gli hook generati supportano infinite query con l’hook useInfiniteQuery di TanStack Query, semplificando l’implementazione di funzionalità “carica più elementi” 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
    }, {
    // Definisci una funzione getNextPageParam per restituire
    // 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 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
    ? 'Caricamento...'
    : items.hasNextPage
    ? 'Carica Altro'
    : 'Nessun altro elemento'}
    </button>
    </div>
    );
    }

    Gli hook generati gestiscono automaticamente la paginazione basata su cursor se l’API la supporta. Il valore nextCursor viene estratto dalla risposta e usato per recuperare la pagina successiva.

    Fai clic qui per un esempio che utilizza il client direttamente.

    Gestione degli Errori

    L’integrazione include una gestione errori con risposte tipizzate. Viene generato un tipo <operation-name>Error che incapsula le possibili risposte di errore definite nella specifica OpenAPI. Ogni errore ha proprietà status e error, e controllando il valore di status puoi identificare 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>Input non valido:</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>Non autorizzato:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error è tipizzato come CreateItem5XXResponse
    return (
    <div>
    <h2>Errore server:</h2>
    <p>{createItem.error.error.message}</p>
    <p>Trace ID: {createItem.error.error.traceId}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Crea Item</button>;
    }
    Fai clic qui per un esempio che utilizza il client direttamente.

    Consumo di uno Stream

    Se hai configurato FastAPI per streammare risposte, l’hook useQuery aggiornerà automaticamente i dati al ricevere nuovi chunk.

    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:

    1. La richiesta HTTP per iniziare lo streaming viene inviata

      • isLoading è true
      • fetchStatus è 'fetching'
      • data è undefined
    2. Ricevuto il primo chunk

      • isLoading diventa false
      • fetchStatus rimane 'fetching'
      • data diventa un array con il primo chunk
    3. Ricevuti chunk successivi

      • isLoading rimane false
      • fetchStatus rimane 'fetching'
      • data viene aggiornato ad ogni nuovo chunk
    4. Stream completato

      • isLoading rimane false
      • fetchStatus diventa 'idle'
      • data contiene tutti i chunk ricevuti
    Fai clic qui per un esempio che utilizza il client direttamente.

    Personalizzazione del Codice Generato

    Query e Mutazioni

    Per default, le operazioni FastAPI che usano metodi HTTP PUT, POST, PATCH e DELETE sono considerate mutazioni. Puoi modificare questo comportamento con 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 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 GET:

    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Cursor di Paginazione Personalizzato

    Per default gli hook assumono un parametro cursor per la paginazione. Puoi personalizzarlo con l’estensione x-cursor:

    @app.get(
    "/items",
    openapi_extra={
    # Specifica un nome diverso per il parametro
    "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
    }

    Per disabilitare infiniteQueryOptions per un endpoint:

    @app.get(
    "/items",
    openapi_extra={
    # Disabilita la paginazione basata su cursor
    "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 metodi client vengono organizzati in base ai tag OpenAPI degli endpoint FastAPI, aiutando a mantenere organizzate le chiamate API.

    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 tag:

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // Operazioni items sotto api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Operazioni users sotto api.users
    const users = useQuery(api.users.list.queryOptions());
    return (
    <div>
    <h2>Items</h2>
    <ul>
    {items.data?.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    <button onClick={() => createItem.mutate({ name: 'New Item' })}>
    Aggiungi Item
    </button>
    <h2>Users</h2>
    <ul>
    {users.data?.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }

    Questo raggruppamento facilita l’organizzazione delle chiamate API e migliora il completamento codice nell’IDE.

    Fai clic qui per un esempio che utilizza il client direttamente.

    Errori

    Puoi personalizzare le risposte di errore in FastAPI definendo classi di eccezione custom, handler di eccezione e specificando modelli di risposta per diversi status code. Il client generato gestirà automaticamente questi tipi di errore.

    Definizione Modelli Errore

    Definisci modelli errore con Pydantic:

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

    Creazione Eccezioni Custom

    Crea classi di eccezione per diversi scenari:

    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 Exception Handlers

    Registra handler per convertire 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

    Specifica i modelli di risposta per diversi status code negli 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 {item_id} non trovato")
    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="Dati item non validi",
    field_errors=["name è obbligatorio"]
    )
    )
    return save_item(item)

    Utilizzo Tipi Errore in React

    Il client gestirà automaticamente questi tipi, permettendo type-check e gestione errori:

    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) => {
    switch (error.status) {
    case 404:
    console.error('Non trovato:', error.error);
    break;
    case 500:
    console.error('Errore server:', error.error.message);
    break;
    }
    }
    });
    // Mutation con gestione errori tipizzata
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    console.error('Errore validazione:', error.error.message);
    console.error('Errori campo:', error.error.field_errors);
    break;
    case 403:
    console.error('Accesso negato:', error.error);
    break;
    }
    }
    });
    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 componente */}</div>;
    }
    Fai clic qui per un esempio che utilizza il client direttamente.

    Best Practices

    Gestione Stati di Caricamento

    Gestisci sempre stati di caricamento ed errori per una migliore UX:

    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:
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    return (
    <ErrorMessage
    message={err.error.message}
    details={`Trace ID: ${err.error.traceId}`}
    />
    );
    default:
    return <ErrorMessage message="Errore sconosciuto" />;
    }
    }
    return (
    <ul>
    {items.data.map((item) => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    );
    }
    Fai clic qui per un esempio che utilizza il client direttamente.

    Aggiornamenti Ottimistici

    Implementa aggiornamenti ottimistici per una UX migliore:

    import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
    function ItemList() {
    const api = useMyApi();
    const queryClient = useQueryClient();
    const itemsQuery = useQuery(api.listItems.queryOptions());
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Cancellazione fallita:', err);
    },
    onSettled: () => {
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    },
    });
    if (itemsQuery.isLoading) {
    return <LoadingSpinner />;
    }
    if (itemsQuery.isError) {
    return <ErrorMessage message="Errore caricamento items" />;
    }
    return (
    <ul>
    {itemsQuery.data.map((item) => (
    <li key={item.id}>
    {item.name}
    <button
    onClick={() => deleteMutation.mutate(item.id)}
    disabled={deleteMutation.isPending}
    >
    {deleteMutation.isPending ? 'Cancellazione...' : 'Elimina'}
    </button>
    </li>
    ))}
    </ul>
    );
    }
    Fai clic qui per un esempio che utilizza il client direttamente.

    Type Safety

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

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    console.log(`Item creato con ID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    createItem.mutate(data);
    };
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    return (
    <FormError
    message="Input non valido"
    errors={error.error.validationErrors}
    />
    );
    case 403:
    return <AuthError reason={error.error.reason} />;
    default:
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: 'New Item' });
    }}>
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? 'Creazione...' : 'Crea Item'}
    </button>
    </form>
    );
    }
    Fai clic qui per un esempio che utilizza il client direttamente.

    I tipi vengono generati automaticamente dallo schema OpenAPI di FastAPI, garantendo che modifiche all’API si riflettano nel frontend dopo una build.