Salta ai contenuti

React a FastAPI

Il generatore connection fornisce un modo per integrare rapidamente il tuo sito web React con il tuo backend FastAPI. Configura tutte le impostazioni necessarie per connettersi ai tuoi backend FastAPI 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 la tua applicazione
  2. Un backend FastAPI funzionante (generato utilizzando il generatore FastAPI)
  3. Cognito Auth aggiunto tramite il generatore ts#website#auth se si connette a un’API che utilizza l’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>,
);

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 nel tuo progetto FastAPI:

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

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
    • 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 Runtime Config all’infrastruttura del tuo sito web se non già presente, il che garantisce che l’URL dell’API per il tuo FastAPI 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 del tuo FastAPI. Questo aggiungerà tre nuovi file alla tua applicazione React:

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

Il client type-safe generato può essere utilizzato per chiamare il tuo FastAPI 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 utilizzando 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 il caricamento di un file, il client generato invia la richiesta come FormData. Definisci un’operazione con un body multipart/form-data nel tuo FastAPI, ad esempio utilizzando UploadFile:

from fastapi import UploadFile
@app.post("/files")
async def upload_file(file: UploadFile, description: str = "") -> FileMetadata:
contents = await file.read()
...

I campi binari sono tipizzati come Blob nel client generato, mentre altri campi (come description sopra) mantengono i loro tipi modellati. Passa un Blob o File — ad esempio uno ottenuto da un <input type="file">:

import { useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function UploadForm() {
const api = useMyApi();
const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadFile.mutate({ file, description: file.name });
}
};
return <input type="file" onChange={handleChange} />;
}

Il client costruisce un body FormData e lascia che fetch imposti automaticamente il Content-Type (incluso il boundary multipart).

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 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 cursor se l’API la supporta. Il valore nextCursor viene estratto dalla risposta e usato per recuperare la pagina successiva.

Clicca qui per un esempio utilizzando direttamente il client.

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

L’esempio seguente presuppone che create_item dichiari le responses definite più avanti in questa pagina — un 400 ValidationErrorDetails, un 403 str e un 500 ErrorDetails. Solo gli stati che dichiari appaiono nell’unione (più il 422 proprio di FastAPI), quindi un case per uno stato che non hai dichiarato è 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 400:
// error.error is typed as ValidationErrorDetails
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
<ul>
{createItem.error.error.fieldErrors.map((err) => (
<li key={err}>{err}</li>
))}
</ul>
</div>
);
case 403:
// error.error is a string as specified in the responses
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error}</p>
</div>
);
case 500:
// error.error is typed as ErrorDetails
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.

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

Per 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 utilizzare le proprietà isLoading e fetchStatus per determinare lo stato corrente dello stream se necessario. Uno stream segue questo ciclo di vita:

  1. La richiesta HTTP per avviare lo streaming viene inviata

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

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

    • isLoading rimane false
    • fetchStatus rimane 'fetching'
    • data viene aggiornato con ogni chunk successivo non appena viene 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 utilizzando direttamente il client vanilla.

Per impostazione predefinita, le operazioni nel tuo FastAPI 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 x-query e x-mutation.

@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());
@app.get(
"/start-processing",
openapi_extra={
"x-mutation": True
}
)
def start_processing():
# ...

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

// Generated hook will include the custom options
const startProcessing = useMutation(api.startProcessing.mutationOptions());

Per impostazione predefinita, gli hook generati assumono la paginazione basata su cursore con un parametro denominato cursor. Puoi personalizzare questo comportamento utilizzando l’estensione x-cursor:

@app.get(
"/items",
openapi_extra={
# Specify a different parameter name for the cursor
"x-cursor": "page_token"
}
)
def list_items(page_token: str = None, limit: int = 10):
# ...
return {
"items": items,
"page_token": next_page_token
}

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

@app.get(
"/items",
openapi_extra={
# Disable cursor-based pagination for this endpoint
"x-cursor": False
}
)
def list_items(page: int = 1, limit: int = 10):
# ...
return {
"items": items,
"total": total_count,
"page": page,
"pages": total_pages
}

Gli hook e i metodi del client generati sono organizzati automaticamente in base ai tag OpenAPI nei tuoi endpoint FastAPI. Questo aiuta a mantenere organizzate le tue chiamate API e rende più facile trovare le operazioni correlate.

All’interno di un gruppo ogni operazione mantiene il proprio nome in camelCase, preso dal nome della funzione del tuo endpoint — quindi def list_items() con tag "items" è raggiungibile a api.items.listItems.

Esempio:

items.py
@app.get(
"/items",
tags=["items"],
)
def list(cursor: str | None = None):
# ...
@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();
// Items operations are grouped under api.items
const items = useQuery(api.items.list.queryOptions({}));
const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.list.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 tue 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 nel FastAPI definendo classi di eccezioni custom, gestori di eccezioni e specificando modelli di risposta per diversi codici di stato. Il client generato gestirà automaticamente questi tipi di errore personalizzati.

Prima, definisci i tuoi modelli di errore utilizzando Pydantic:

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

Quindi crea classi di eccezione personalizzate 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: ValidationErrorDetails):
self.details = details

Registra i gestori di eccezioni 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(),
)

Infine, specifica i modelli di risposta per diversi codici di stato di errore nelle definizioni dei tuoi 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": ValidationErrorDetails},
403: {"model": str},
500: {"model": ErrorDetails},
}
)
def create_item(item: Item) -> Item:
if not is_valid(item):
raise ValidationException(
ValidationErrorDetails(
message="Invalid item data",
field_errors=["name is required"]
)
)
return save_item(item)

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 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 responses in your FastAPI
switch (error.status) {
case 400:
// error.error is typed as ValidationErrorDetails
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is a string as specified in the responses
console.error('Forbidden:', error.error);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
switch (getItem.error.status) {
case 404:
// error.error is a string as specified in the responses
return <NotFoundMessage message={getItem.error.error} />;
case 500:
// error.error is typed as ErrorDetails
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 e di 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 a string as specified in the responses
return <ErrorMessage message={err.error} />;
case 500:
// err.error is typed as ErrorDetails
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 { ListItemsOutput } 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<ListItemsOutput>(
api.listItems.queryKey({}),
);
// Optimistically update to the new value
queryClient.setQueryData<ListItemsOutput>(
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 una completa type safety end-to-end. Il tuo IDE fornirà completamento automatico completo e controllo dei tipi per tutte le tue 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 ValidationErrorDetails
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is a string as specified in the responses
return <AuthError reason={error.error} />;
case 500:
// error.error is typed as ErrorDetails
return <ServerError message={error.error.message} />;
default:
// FastAPI adds a 422 to every endpoint, so `default` isn't narrowed
return <ServerError message="An unknown error occurred" />;
}
}
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 vengono generati automaticamente dallo schema OpenAPI del tuo FastAPI, garantendo che eventuali modifiche alla tua API si riflettano nel codice frontend dopo una build.

Se il tuo FastAPI 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 <ApiName>Provider.tsx generato e aggiungi il tuo token o chiave API agli header della richiesta.