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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParametroTipoPredefinitoDescrizione
sourceProject Obbligatoriostring-Il progetto sorgente
targetProject Obbligatoriostring-Il progetto di destinazione a cui connettersi
sourceComponent string-Il componente sorgente da cui connettersi (nome del componente, percorso relativo alla radice del progetto sorgente, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come sorgente.
targetComponent string-Il componente di destinazione a cui connettersi (nome del componente, percorso relativo alla radice del progetto di destinazione, o id del generatore). Usare '.' per selezionare esplicitamente il progetto come destinazione.
preferInstallDependencies booleantrueSe preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine.

Il generatore apporterà modifiche ai seguenti file 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.

Dipendenza File Watcher

watch-generate:<ApiName>-client si basa sul comando nx watch, che richiede che il Nx Daemon sia in esecuzione. Pertanto, se hai disabilitato il daemon, il client non si rigenererà automaticamente quando apporti modifiche al tuo FastAPI.

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

Puoi utilizzare il metodo queryOptions per recuperare le opzioni necessarie per chiamare la tua API utilizzando l’hook useQuery di TanStack Query:

import { useQuery } from '@tanstack/react-query';
import { useState, useEffect } from 'react';
import { useMyApi } from './hooks/useMyApi';
function MyComponent() {
const api = useMyApi();
const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
if (item.isLoading) return <div>Loading...</div>;
if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;
}
Clicca qui per un esempio 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: {createItem.error.message}
</div>
)}
</form>
);
}

Puoi anche aggiungere callback per diversi stati della mutation:

const createItem = useMutation({
...api.createItem.mutationOptions(),
onSuccess: (data) => {
// This will run when the mutation succeeds
console.log('Item created:', data);
// You can navigate to the new item
navigate(`/items/${data.id}`);
},
onError: (error) => {
// This will run when the mutation fails
console.error('Failed to create item:', error);
},
onSettled: () => {
// This will run when the mutation completes (success or error)
// Good place to invalidate queries that might be affected
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
}
});
Clicca qui per un esempio utilizzando direttamente il client.

Per gli endpoint che accettano 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.isLoading) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={items.error.message} />;
}
return (
<div>
{/* Flatten the pages array to render all items */}
<ul>
{items.data.pages.flatMap(page =>
page.items.map(item => (
<li key={item.id}>{item.name}</li>
))
)}
</ul>
<button
onClick={() => items.fetchNextPage()}
disabled={!items.hasNextPage || items.isFetchingNextPage}
>
{items.isFetchingNextPage
? 'Loading more...'
: items.hasNextPage
? 'Load More'
: 'No more items'}
</button>
</div>
);
}

Gli hook generati gestiscono automaticamente la paginazione basata su 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 <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 is typed as 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 is typed as CreateItem403Response
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
case 502:
// error.error is typed as CreateItem5XXResponse
return (
<div>
<h2>Server error:</h2>
<p>{createItem.error.error.message}</p>
<p>Trace ID: {createItem.error.error.traceId}</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.

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();
// 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 ValidationError(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: ValidationError):
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": 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 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();
// Query with typed error handling
const getItem = useQuery({
...api.getItem.queryOptions({ itemId: '123' }),
onError: (error) => {
// Error is typed based on the responses in your FastAPI
switch (error.status) {
case 404:
// error.error is a string as specified in the responses
console.error('Not found:', error.error);
break;
case 500:
// error.error is typed as ErrorDetails
console.error('Server error:', error.error.message);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as ValidationError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.field_errors);
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) {
if (getItem.error.status === 404) {
return <NotFoundMessage message={getItem.error.error} />;
} else {
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 typed as ListItems403Response
return <ErrorMessage message={err.error.reason} />;
case 500:
case 502:
// err.error is typed as ListItems5XXResponse
return (
<ErrorMessage
message={err.error.message}
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 utilizzando direttamente il client vanilla.

Implementa aggiornamenti ottimistici per una migliore esperienza utente:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() {
const api = useMyApi();
const queryClient = useQueryClient();
// Query to fetch items
const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates
const deleteMutation = useMutation({
...api.deleteItem.mutationOptions(),
onMutate: async (itemId) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value
const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value
queryClient.setQueryData(
api.listItems.queryKey(),
(old) => old.filter((item) => item.id !== itemId)
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, itemId, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
console.error('Failed to delete item:', err);
},
onSettled: () => {
// Always refetch after error or success to ensure data is in sync with server
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
},
});
if (itemsQuery.isLoading) {
return <LoadingSpinner />;
}
if (itemsQuery.isError) {
return <ErrorMessage message="Failed to load items" />;
}
return (
<ul>
{itemsQuery.data.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate(item.id)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
);
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

L’integrazione fornisce 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 CreateItem400Response
return (
<FormError
message="Invalid input"
errors={error.error.validationErrors}
/>
);
case 403:
// error.error is typed as CreateItem403Response
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as CreateItem5XXResponse for 500, 502, etc.
return <ServerError message={error.error.message} />;
}
}
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({ name: 'New Item' });
}}>
{/* Form fields */}
<button
type="submit"
disabled={createItem.isPending}
>
{createItem.isPending ? 'Creating...' : 'Create Item'}
</button>
</form>
);
}
Clicca qui per un esempio utilizzando direttamente il client vanilla.

I tipi 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.