React a FastAPI
El generador connection proporciona una forma de integrar rápidamente tu sitio web React con tu backend FastAPI. Configura toda la configuración necesaria para conectarse a tus backends FastAPI de manera segura en cuanto a tipos, incluyendo la generación de cliente y hooks de TanStack Query, soporte de autenticación AWS IAM y Cognito, y manejo adecuado de errores.
Requisitos previos
Sección titulada «Requisitos previos»Antes de usar este generador, asegúrate de que tu aplicación React tenga:
- Un archivo
main.tsxque renderice tu aplicación - Un backend FastAPI funcional (generado usando el generador FastAPI)
- Cognito Auth agregado a través del generador
ts#website#authsi conectas una API que usa autenticación Cognito o IAM
Ejemplo de estructura requerida de 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>,);Ejecutar el generador
Sección titulada «Ejecutar el generador»pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:connection --dry-runyarn nx g @aws/nx-plugin:connection --dry-runnpx nx g @aws/nx-plugin:connection --dry-runbunx nx g @aws/nx-plugin:connection --dry-run- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- Haga clic en
Generate
Opciones
Sección titulada «Opciones»| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
| sourceProject Requerido | string | - | El proyecto de origen |
| targetProject Requerido | string | - | El proyecto de destino al que conectar |
| sourceComponent | string | - | El componente de origen desde el que conectar (nombre del componente, ruta relativa a la raíz del proyecto de origen, o id del generador). Use '.' para seleccionar explícitamente el proyecto como origen. |
| targetComponent | string | - | El componente de destino al que conectar (nombre del componente, ruta relativa a la raíz del proyecto de destino, o id del generador). Use '.' para seleccionar explícitamente el proyecto como destino. |
| preferInstallDependencies | boolean | true | Si se prefiere instalar las dependencias después de que se ejecute el generador. Establecer en false para diferir la instalación al ejecutar múltiples generadores en lote (la instalación aún se ejecuta si es necesario para que los generadores subsiguientes puedan calcular el grafo de proyectos de Nx); instalar una vez al final. |
Salida del generador
Sección titulada «Salida del generador»El generador realizará cambios en los siguientes archivos en tu proyecto FastAPI:
Directorioscripts
- generate_open_api.py Agrega un script que genera una especificación OpenAPI para tu API
- project.json Se agrega un nuevo target al build que invoca el script de generación anterior
El generador realizará cambios en los siguientes archivos en tu aplicación React:
Directoriosrc
Directoriocomponents
- <ApiName>Provider.tsx Proveedor para tu cliente API
- QueryClientProvider.tsx Proveedor de cliente TanStack React Query
Directoriohooks
- use<ApiName>.tsx Agrega un hook para llamar a tu API con estado gestionado por TanStack Query
- use<ApiName>Client.tsx Agrega un hook para instanciar el cliente API vanilla que puede llamar a tu API.
- useSigV4.tsx Agrega un hook para firmar solicitudes HTTP con SigV4 (si seleccionaste autenticación IAM)
- project.json Se agrega un nuevo target al build que genera un cliente seguro en cuanto a tipos
- .gitignore Los archivos de cliente generados son ignorados por defecto
El generador también agregará Runtime Config a la infraestructura de tu sitio web si no está presente ya, lo que garantiza que la URL de la API para tu FastAPI esté disponible en el sitio web y se configure automáticamente mediante el hook use<ApiName>.tsx.
Generación de código
Sección titulada «Generación de código»En tiempo de compilación, se genera un cliente seguro en cuanto a tipos a partir de la especificación OpenAPI de tu FastAPI. Esto agregará tres nuevos archivos a tu aplicación React:
Directoriosrc
Directoriogenerated
Directorio<ApiName>
- types.gen.ts Tipos generados a partir de los modelos pydantic definidos en tu FastAPI
- client.gen.ts Cliente seguro en cuanto a tipos para llamar a tu API
- options-proxy.gen.ts Proporciona métodos para crear opciones de hooks TanStack Query para interactuar con tu API usando TanStack Query
Usar el código generado
Sección titulada «Usar el código generado»El cliente seguro en cuanto a tipos generado se puede usar para llamar a tu FastAPI desde tu aplicación React. Se recomienda hacer uso del cliente a través de los hooks TanStack Query, pero puedes usar el cliente vanilla si lo prefieres.
Dependencia del observador de archivos
watch-generate:<ApiName>-client depende del comando nx watch, que requiere que el Nx Daemon esté en ejecución. Por lo tanto, si has deshabilitado el daemon, el cliente no se regenerará automáticamente al realizar cambios en tu FastAPI.
Usar el hook de API
Sección titulada «Usar el hook de API»El generador proporciona un hook use<ApiName> que puedes usar para llamar a tu API con TanStack Query.
Consultas
Sección titulada «Consultas»Puedes usar el método queryOptions para obtener las opciones requeridas para llamar a tu API usando el 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>;}Usar el cliente API directamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}Mutaciones
Sección titulada «Mutaciones»Los hooks generados incluyen soporte para mutaciones usando el hook useMutation de TanStack Query. Esto provee una forma limpia de manejar operaciones de creación, actualización y eliminación con estados de carga, manejo de errores y actualizaciones optimistas.
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> );}También puedes añadir callbacks para diferentes estados de la mutación:
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() }); }});Mutaciones usando el cliente API directamente
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}Carga de archivos
Sección titulada «Carga de archivos»Para endpoints que aceptan una carga de archivo, el cliente generado envía la solicitud como FormData. Define una operación con un cuerpo multipart/form-data en tu FastAPI, por ejemplo usando UploadFile:
from fastapi import UploadFile
@app.post("/files")async def upload_file(file: UploadFile, description: str = "") -> FileMetadata: contents = await file.read() ...Los campos binarios se tipan como Blob en el cliente generado, y otros campos (como description arriba) mantienen sus tipos modelados. Pasa un Blob o File — por ejemplo, uno obtenido de 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} />;}El cliente construye un cuerpo FormData y permite que fetch establezca el Content-Type (incluyendo el límite multipart) automáticamente.
Paginación con consultas infinitas
Sección titulada «Paginación con consultas infinitas»Para endpoints que aceptan un parámetro cursor como entrada, los hooks generados proporcionan soporte para consultas infinitas usando el hook useInfiniteQuery de TanStack Query. Esto facilita la implementación de funcionalidad de “cargar más” o desplazamiento 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> );}Los hooks generados manejan automáticamente la paginación basada en cursor si tu API lo soporta. El valor nextCursor se extrae de la respuesta y se usa para obtener la próxima página.
Paginación usando el cliente API directamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Fetch initial data useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Function to load more items const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}Manejo de errores
Sección titulada «Manejo de errores»La integración incluye manejo de errores incorporado con respuestas de error tipadas. Se genera un tipo <operation-name>Error que encapsula las posibles respuestas de error definidas en la especificación OpenAPI. Cada error tiene una propiedad status y error, y al verificar el valor de status puedes reducir a un tipo específico de error.
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>;}Manejo de errores usando el cliente API directamente
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> <ul> {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>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Consumir un stream
Sección titulada «Consumir un stream»Si has configurado tu FastAPI para transmitir respuestas, tu hook useQuery actualizará automáticamente sus datos a medida que lleguen nuevos fragmentos del stream.
Por ejemplo:
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> );}Puedes usar las propiedades isLoading y fetchStatus para determinar el estado actual del stream si es necesario. Un stream sigue este ciclo de vida:
-
Se envía la solicitud HTTP para iniciar el streaming
isLoadingestruefetchStatuses'fetching'dataesundefined
-
Se recibe el primer fragmento del stream
isLoadingse convierte enfalsefetchStatuspermanece en'fetching'datase convierte en un array que contiene el primer fragmento
-
Se reciben fragmentos subsiguientes
isLoadingpermanece enfalsefetchStatuspermanece en'fetching'datase actualiza con cada fragmento subsiguiente tan pronto como se recibe
-
El stream se completa
isLoadingpermanece enfalsefetchStatusse convierte en'idle'dataes un array de todos los fragmentos recibidos
Streaming usando el cliente API directamente
Si has configurado tu FastAPI para transmitir respuestas>, el cliente generado incluirá métodos seguros en cuanto a tipos para iterar asincrónicamente sobre fragmentos en tu stream usando la sintaxis for await.
Por ejemplo:
function MyStreamingComponent() { const api = useMyApiClient();
const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}Personalizar el código generado
Sección titulada «Personalizar el código generado»Consultas y Mutaciones
Sección titulada «Consultas y Mutaciones»Por defecto, las operaciones en tu FastAPI que usan los métodos HTTP PUT, POST, PATCH y DELETE se consideran mutaciones, y las demás se consideran consultas.
Puedes cambiar este comportamiento usando x-query y x-mutation.
x-query
Sección titulada «x-query»@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...El hook generado proveerá queryOptions aunque use el método HTTP POST:
const items = useQuery(api.listItems.queryOptions());x-mutation
Sección titulada «x-mutation»@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...El hook generado proveerá mutationOptions aunque use el método HTTP GET:
// Generated hook will include the custom optionsconst startProcessing = useMutation(api.startProcessing.mutationOptions());Cursor de paginación personalizado
Sección titulada «Cursor de paginación personalizado»Por defecto, los hooks generados asumen paginación basada en cursor con un parámetro llamado cursor. Puedes personalizar este comportamiento usando la extensión 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 no quieres generar infiniteQueryOptions para una operación, puedes establecer x-cursor en 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 }Agrupar operaciones
Sección titulada «Agrupar operaciones»Los hooks generados y los métodos del cliente se organizan automáticamente según las etiquetas OpenAPI en tus endpoints FastAPI. Esto ayuda a mantener tus llamadas API organizadas y facilita encontrar operaciones relacionadas.
Por ejemplo:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@app.get( "/users", tags=["users"],)def list(): # ...Los hooks generados se agruparán por estas etiquetas:
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> );}Esta agrupación facilita la organización de tus llamadas API y proporciona una mejor finalización de código en tu IDE.
Operaciones agrupadas usando el cliente API directamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Errores
Sección titulada «Errores»Puedes personalizar las respuestas de error en tu FastAPI definiendo clases de excepción personalizadas, manejadores de excepciones y especificando modelos de respuesta para diferentes códigos de estado. El cliente generado manejará automáticamente estos tipos de error personalizados.
Definir modelos de error personalizados
Sección titulada «Definir modelos de error personalizados»Primero, define tus modelos de error usando Pydantic:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]Crear excepciones personalizadas
Sección titulada «Crear excepciones personalizadas»Luego crea clases de excepción para diferentes escenarios de error:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = detailsAgregar manejadores de excepciones
Sección titulada «Agregar manejadores de excepciones»Registra manejadores de excepciones para convertir tus excepciones en respuestas HTTP:
from fastapi import Requestfrom 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(), )Especificar modelos de respuesta
Sección titulada «Especificar modelos de respuesta»Finalmente, especifica los modelos de respuesta para diferentes códigos de estado en tus endpoints:
@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)Usar tipos de error personalizados en React
Sección titulada «Usar tipos de error personalizados en React»El cliente generado manejará automáticamente estos tipos de error personalizados, permitiéndote verificar tipos y manejar diferentes respuestas de error:
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> );}Manejar errores personalizados con el cliente directamente
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the responses in your FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is a string as specified in the responses console.error('Not found:', err.error); break; case 500: // err.error is typed as ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error is a string as specified in the responses console.error('Forbidden:', err.error); break; } } };
// Component rendering with error handling if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}Mejores prácticas
Sección titulada «Mejores prácticas»Manejar estados de carga
Sección titulada «Manejar estados de carga»Siempre maneja estados de carga y errores para una mejor experiencia de usuario:
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> );}Manejar estados de carga usando el cliente API directamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; 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.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Actualizaciones optimistas
Sección titulada «Actualizaciones optimistas»Implementa actualizaciones optimistas para una mejor experiencia de usuario:
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> );}Actualizaciones optimistas usando el cliente API directamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}Seguridad de tipos
Sección titulada «Seguridad de tipos»La integración proporciona seguridad de tipos completa de extremo a extremo. Tu IDE proporcionará autocompletado completo y verificación de tipos para todas tus llamadas 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> );}Seguridad de tipos usando el cliente API directamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error is typed as CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error is typed as CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}Los tipos se generan automáticamente a partir del esquema OpenAPI de tu FastAPI, asegurando que cualquier cambio en tu API se refleje en tu código frontend después de una compilación.
Autenticación personalizada
Sección titulada «Autenticación personalizada»Si tu FastAPI usa autenticación Custom (Lambda Authorizer), necesitarás editar el proveedor de cliente generado para agregar los encabezados de autorización que tu autorizador espera. Busca la configuración fetch en el <ApiName>Provider.tsx generado y agrega tu token o clave API a los encabezados de la solicitud.