Aller au contenu

React vers FastAPI

Le générateur connection fournit un moyen d’intégrer rapidement votre site web React avec votre backend FastAPI. Il configure toute la configuration nécessaire pour se connecter à vos backends FastAPI de manière type-safe, y compris la génération de client et de hooks TanStack Query, la prise en charge de l’authentification AWS IAM et Cognito et une gestion appropriée des erreurs.

Avant d’utiliser ce générateur, assurez-vous que votre application React dispose de :

  1. Un fichier main.tsx qui rend votre application
  2. Un backend FastAPI fonctionnel (généré à l’aide du générateur FastAPI)
  3. Cognito Auth ajouté via le générateur ts#website#auth si vous vous connectez à une API qui utilise l’authentification Cognito ou IAM
Exemple de structure main.tsx requise
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
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParamètreTypePar défautDescription
sourceProject Requisstring-Le projet source
targetProject Requisstring-Le projet cible auquel se connecter
sourceComponent string-Le composant source depuis lequel se connecter (nom du composant, chemin relatif à la racine du projet source, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme source.
targetComponent string-Le composant cible auquel se connecter (nom du composant, chemin relatif à la racine du projet cible, ou identifiant du générateur). Utilisez '.' pour sélectionner explicitement le projet comme cible.
preferInstallDependencies booleantrueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin.

Le générateur apportera des modifications aux fichiers suivants dans votre projet FastAPI :

  • Répertoirescripts
    • generate_open_api.py Ajoute un script qui génère une spécification OpenAPI pour votre API
  • project.json Une nouvelle cible est ajoutée au build qui invoque le script de génération ci-dessus

Le générateur apportera des modifications aux fichiers suivants dans votre application React :

  • Répertoiresrc
    • Répertoirecomponents
      • <ApiName>Provider.tsx Provider pour votre client API
      • QueryClientProvider.tsx Provider du client TanStack React Query
    • Répertoirehooks
      • use<ApiName>.tsx Ajoute un hook pour appeler votre API avec l’état géré par TanStack Query
      • use<ApiName>Client.tsx Ajoute un hook pour instancier le client API vanilla qui peut appeler votre API.
      • useSigV4.tsx Ajoute un hook pour signer les requêtes HTTP avec SigV4 (si vous avez sélectionné l’authentification IAM)
  • project.json Une nouvelle cible est ajoutée au build qui génère un client type-safe
  • .gitignore Les fichiers client générés sont ignorés par défaut

Le générateur ajoutera également Runtime Config à votre infrastructure de site web si elle n’est pas déjà présente, ce qui garantit que l’URL de l’API pour votre FastAPI est disponible dans le site web et automatiquement configurée par le hook use<ApiName>.tsx.

Au moment du build, un client type-safe est généré à partir de la spécification OpenAPI de votre FastAPI. Cela ajoutera trois nouveaux fichiers à votre application React :

  • Répertoiresrc
    • Répertoiregenerated
      • Répertoire<ApiName>
        • types.gen.ts Types générés à partir des modèles pydantic définis dans votre FastAPI
        • client.gen.ts Client type-safe pour appeler votre API
        • options-proxy.gen.ts Fournit des méthodes pour créer des options de hooks TanStack Query pour interagir avec votre API en utilisant TanStack Query

Le client type-safe généré peut être utilisé pour appeler votre FastAPI depuis votre application React. Il est recommandé d’utiliser le client via les hooks TanStack Query, mais vous pouvez utiliser le client vanilla si vous préférez.

Dépendance du File Watcher

watch-generate:<ApiName>-client s’appuie sur la commande nx watch, qui nécessite que le Nx Daemon soit en cours d’exécution. Par conséquent, si vous avez désactivé le daemon, le client ne se régénérera pas automatiquement lors de modifications de votre FastAPI.

Le générateur fournit un hook use<ApiName> que vous pouvez utiliser pour appeler votre API avec TanStack Query.

Vous pouvez utiliser la méthode queryOptions pour récupérer les options requises pour appeler votre API en utilisant le hook useQuery de 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>;
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

Les hooks générés incluent la prise en charge des mutations à l’aide du hook useMutation de TanStack Query. Cela fournit un moyen propre de gérer les opérations de création, de mise à jour et de suppression avec des états de chargement, une gestion des erreurs et des mises à jour optimistes.

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>
);
}

Vous pouvez également ajouter des callbacks pour différents états de 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() });
}
});
Cliquez ici pour un exemple utilisant directement le client.

Pour les endpoints qui acceptent un téléchargement de fichier, le client généré envoie la requête en tant que FormData. Définissez une opération avec un corps multipart/form-data dans votre FastAPI, par exemple en utilisant UploadFile :

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

Les champs binaires sont typés comme Blob sur le client généré, et les autres champs (comme description ci-dessus) conservent leurs types modélisés. Passez un Blob ou File — par exemple un obtenu à partir d’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} />;
}

Le client construit un corps FormData et laisse fetch définir automatiquement le Content-Type (y compris la limite multipart).

Pour les endpoints qui acceptent un paramètre cursor en entrée, les hooks générés fournissent la prise en charge des requêtes infinies à l’aide du hook useInfiniteQuery de TanStack Query. Cela facilite l’implémentation de la fonctionnalité “charger plus” ou de défilement infini.

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>
);
}

Les hooks générés gèrent automatiquement la pagination basée sur le curseur si votre API la prend en charge. La valeur nextCursor est extraite de la réponse et utilisée pour récupérer la page suivante.

Cliquez ici pour un exemple utilisant directement le client.

L’intégration inclut une gestion des erreurs intégrée avec des réponses d’erreur typées. Un type <operation-name>Error est généré qui encapsule les réponses d’erreur possibles définies dans la spécification OpenAPI. Chaque erreur a une propriété status et error, et en vérifiant la valeur de status, vous pouvez réduire à un type d’erreur spécifique.

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>;
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

Si vous avez configuré votre FastAPI pour diffuser des réponses, votre hook useQuery mettra automatiquement à jour ses données à mesure que de nouveaux morceaux du flux arrivent.

Par exemple :

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>
);
}

Vous pouvez utiliser les propriétés isLoading et fetchStatus pour déterminer l’état actuel du flux si nécessaire. Un flux suit ce cycle de vie :

  1. La requête HTTP pour démarrer le streaming est envoyée

    • isLoading est true
    • fetchStatus est 'fetching'
    • data est undefined
  2. Le premier morceau du flux est reçu

    • isLoading devient false
    • fetchStatus reste 'fetching'
    • data devient un tableau contenant le premier morceau
  3. Les morceaux suivants sont reçus

    • isLoading reste false
    • fetchStatus reste 'fetching'
    • data est mis à jour avec chaque morceau suivant dès qu’il est reçu
  4. Le flux se termine

    • isLoading reste false
    • fetchStatus devient 'idle'
    • data est un tableau de tous les morceaux reçus
Cliquez ici pour un exemple utilisant directement le client vanilla.

Par défaut, les opérations dans votre FastAPI qui utilisent les méthodes HTTP PUT, POST, PATCH et DELETE sont considérées comme des mutations, et toutes les autres sont considérées comme des requêtes.

Vous pouvez modifier ce comportement en utilisant x-query et x-mutation.

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

Le hook généré fournira queryOptions même s’il utilise la méthode HTTP POST :

const items = useQuery(api.listItems.queryOptions());
@app.get(
"/start-processing",
openapi_extra={
"x-mutation": True
}
)
def start_processing():
# ...

Le hook généré fournira mutationOptions même s’il utilise la méthode HTTP GET :

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

Par défaut, les hooks générés supposent une pagination basée sur le curseur avec un paramètre nommé cursor. Vous pouvez personnaliser ce comportement en utilisant l’extension 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
}

Si vous ne souhaitez pas générer infiniteQueryOptions pour une opération, vous pouvez définir x-cursor sur 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
}

Les hooks et méthodes client générés sont automatiquement organisés en fonction des tags OpenAPI dans vos endpoints FastAPI. Cela aide à garder vos appels API organisés et facilite la recherche d’opérations liées.

Par exemple :

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():
# ...

Les hooks générés seront regroupés par ces tags :

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>
);
}

Ce regroupement facilite l’organisation de vos appels API et fournit une meilleure complétion de code dans votre IDE.

Cliquez ici pour un exemple utilisant directement le client.

Vous pouvez personnaliser les réponses d’erreur dans votre FastAPI en définissant des classes d’exception personnalisées, des gestionnaires d’exceptions et en spécifiant des modèles de réponse pour différents codes d’état d’erreur. Le client généré gérera automatiquement ces types d’erreur personnalisés.

Tout d’abord, définissez vos modèles d’erreur en utilisant Pydantic :

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

Ensuite, créez des classes d’exception personnalisées pour différents scénarios d’erreur :

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

Enregistrez des gestionnaires d’exceptions pour convertir vos exceptions en réponses 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(),
)

Enfin, spécifiez les modèles de réponse pour différents codes d’état d’erreur dans vos définitions d’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)

Utilisation de types d’erreur personnalisés dans React

Section intitulée « Utilisation de types d’erreur personnalisés dans React »

Le client généré gérera automatiquement ces types d’erreur personnalisés, vous permettant de vérifier le type et de gérer différentes réponses d’erreur :

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>
);
}
Cliquez ici pour un exemple utilisant directement le client.

Gérez toujours les états de chargement et d’erreur pour une meilleure expérience utilisateur :

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>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

Implémentez des mises à jour optimistes pour une meilleure expérience utilisateur :

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>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

L’intégration fournit une sécurité des types complète de bout en bout. Votre IDE fournira une autocomplétion complète et une vérification de type pour tous vos appels 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>
);
}
Cliquez ici pour un exemple utilisant directement le client vanilla.

Les types sont automatiquement générés à partir du schéma OpenAPI de votre FastAPI, garantissant que toute modification de votre API est reflétée dans votre code frontend après un build.

Si votre FastAPI utilise l’authentification Custom (Lambda Authorizer), vous devrez modifier le provider client généré pour ajouter les en-têtes d’autorisation attendus par votre authorizer. Recherchez la configuration fetch dans le <ApiName>Provider.tsx généré et ajoutez votre token ou clé API aux en-têtes de requête.