React a FastAPI
El generador api-connection
proporciona una forma rápida de integrar tu sitio web en React con tu backend en FastAPI. Configura toda la configuración necesaria para conectar con tus backends FastAPI de manera tipada, incluyendo la generación de clientes y hooks de TanStack Query, soporte para autenticación AWS IAM y Cognito, y manejo adecuado de errores.
Requisitos previos
Antes de usar este generador, asegúrate que tu aplicación React tenga:
- Un archivo
main.tsx
que renderice tu aplicación - Un backend FastAPI funcional (generado usando el generador FastAPI)
- Autenticación Cognito añadida mediante el generador
ts#cloudscape-website-auth
si conectas una API que use autenticación Cognito o IAM
Ejemplo de estructura requerida en 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 HTMTMLElement,);root.render( <StrictMode> <App /> </StrictMode>,);
Uso
Ejecutar el generador
- 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 - api-connection
- Complete los parámetros requeridos
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
Opciones
Parámetro | Tipo | Predeterminado | Descripción |
---|---|---|---|
sourceProject Requerido | string | - | The source project which will call the API |
targetProject Requerido | string | - | The target project containing your API |
Salida del generador
El generador modificará los siguientes archivos en tu proyecto FastAPI:
Directoryscripts
- generate_open_api.py Agrega un script que genera una especificación OpenAPI para tu API
- project.json Se añade un nuevo target al build que invoca el script anterior
El generador modificará los siguientes archivos en tu aplicación React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Proveedor para el cliente de tu API
- QueryClientProvider.tsx Proveedor del cliente de TanStack React Query
Directoryhooks
- use<ApiName>.tsx Añade un hook para llamar a tu API con estado gestionado por TanStack Query
- use<ApiName>Client.tsx Añade un hook para instanciar el cliente vanilla que puede llamar a tu API
- useSigV4.tsx Añade un hook para firmar peticiones HTTP con SigV4 (si seleccionaste autenticación IAM)
- project.json Se añade un nuevo target al build que genera un cliente tipado
- .gitignore Los archivos del cliente generado se ignoran por defecto
El generador también añadirá Runtime Config a la infraestructura de tu sitio web si no está presente, asegurando que la URL de la API para tu FastAPI esté disponible en el sitio y configurada automáticamente por el hook use<ApiName>.tsx
.
Generación de código
Durante el build, se genera un cliente tipado a partir de la especificación OpenAPI de tu FastAPI. Esto añadirá tres nuevos archivos a tu aplicación React:
Directorysrc
Directorygenerated
Directory<ApiName>
- types.gen.ts Tipos generados de los modelos pydantic definidos en tu FastAPI
- client.gen.ts Cliente tipado para llamar a tu API
- options-proxy.gen.ts Provee métodos para crear opciones de hooks TanStack Query para interactuar con tu API
Por defecto, el cliente generado se ignora del control de versiones. Si prefieres incluirlo, puedes eliminar la entrada del archivo .gitignore
de tu aplicación React, pero ten en cuenta que cualquier cambio manual en los archivos .gen.ts
será sobrescrito al construir el proyecto.
Usando el código generado
El cliente tipado generado puede usarse para llamar a tu FastAPI desde tu aplicación React. Se recomienda usarlo mediante los hooks de TanStack Query, pero también puedes usar el cliente vanilla si lo prefieres.
Cada vez que hagas cambios en tu FastAPI, necesitas reconstruir tu proyecto para que se reflejen en el cliente generado. Por ejemplo:
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
Si estás trabajando activamente en tu aplicación React y FastAPI simultáneamente, usa el target serve-local
de tu aplicación React, que regenerará automáticamente el cliente cuando cambie tu API, además de recargar en caliente tu sitio y servidor FastAPI local:
pnpm nx run <WebsiteProject>:serve-local
yarn nx run <WebsiteProject>:serve-local
npx nx run <WebsiteProject>:serve-local
bunx nx run <WebsiteProject>:serve-local
Para un control más granular, puedes usar el target watch-generate:<ApiName>-client
de tu aplicación React para regenerar el cliente cada vez que hagas cambios en la API:
pnpm nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
yarn nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
npx nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
bunx nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
Usando el hook de la API
El generador provee un hook use<ApiName>
que puedes usar para llamar a tu API con TanStack Query.
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 de 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
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(); // Crea una mutación usando las opciones generadas const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Campos del formulario */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creando...' : 'Crear ítem'} </button>
{createItem.isSuccess && ( <div className="success"> Ítem creado con 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) => { // Se ejecuta cuando la mutación tiene éxito console.log('Ítem creado:', data); // Puedes navegar al nuevo ítem navigate(`/items/${data.id}`); }, onError: (error) => { // Se ejecuta cuando la mutación falla console.error('Error al crear ítem:', error); }, onSettled: () => { // Se ejecuta cuando la mutación finaliza (éxito o error) // Buen lugar para invalidar consultas afectadas queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutaciones usando el cliente de 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); // Puedes navegar al nuevo ítem // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Error al crear ítem:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Campos del formulario */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creando...' : 'Crear ítem'} </button>
{createdItem && ( <div className="success"> Ítem creado con ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}
Paginación con consultas infinitas
Para endpoints que aceptan un parámetro cursor
como entrada, los hooks generados proveen soporte para consultas infinitas usando el hook useInfiniteQuery
de TanStack Query. Esto facilita implementar funcionalidad de “cargar más” o scroll infinito.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Número de ítems por página }, { // Asegúrate de definir una función getNextPageParam que retorne // el parámetro que se pasará como 'cursor' para la próxima página getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Aplana el array de páginas para renderizar todos los ítems */} <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 ? 'Cargando más...' : items.hasNextPage ? 'Cargar más' : 'No hay más ítems'} </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.
Si tienes una API paginada cuyo parámetro de paginación tiene un nombre distinto a cursor
, puedes personalizarlo usando la extensión OpenAPI x-cursor
.
Paginación usando el cliente de 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);
// Obtener datos iniciales 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]);
// Función para cargar más ítems 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 ? 'Cargando más...' : nextCursor ? 'Cargar más' : 'No hay más ítems'} </button> </div> );}
Manejo de errores
La integración incluye manejo de errores integrado con respuestas 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 restringir 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 está tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</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 está tipado como CreateItem403Response return ( <div> <h2>No autorizado:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error está tipado como CreateItem5XXResponse return ( <div> <h2>Error del servidor:</h2> <p>{createItem.error.error.message}</p> <p>Trace ID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Crear ítem</button>;}
Manejo de errores usando el cliente de 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 está tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</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 está tipado como CreateItem403Response return ( <div> <h2>No autorizado:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error está tipado como CreateItem5XXResponse return ( <div> <h2>Error del servidor:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Crear ítem</button>;}
Consumir un stream
Si has configurado tu FastAPI para transmitir respuestas, tu hook useQuery
se actualizará automáticamente con nuevos datos a medida que lleguen 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 petición HTTP para iniciar el stream
isLoading
estrue
fetchStatus
es'fetching'
data
esundefined
-
Se recibe el primer fragmento del stream
isLoading
cambia afalse
fetchStatus
permanece'fetching'
data
se convierte en un array con el primer fragmento
-
Se reciben fragmentos posteriores
isLoading
permanecefalse
fetchStatus
permanece'fetching'
data
se actualiza con cada nuevo fragmento recibido
-
El stream finaliza
isLoading
permanecefalse
fetchStatus
cambia a'idle'
data
es un array con todos los fragmentos recibidos
Streaming usando el cliente de API directamente
Si has configurado tu FastAPI para transmitir respuestas, el cliente generado incluirá métodos tipados para iterar asíncronamente sobre fragmentos del stream usando 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> );}
Si tienes una API de streaming que acepta un parámetro cursor
, cuando uses el hook useInfiniteQuery
, cada página esperará a que el stream finalice antes de cargarse.
Personalización del código generado
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
@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
@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
El hook generado proveerá mutationOptions
aunque use el método HTTP GET
:
// El hook generado incluirá las opciones personalizadasconst startProcessing = useMutation(api.startProcessing.mutationOptions());
Cursor de paginación personalizado
Por defecto, los hooks generados asumen paginación basada en cursor con un parámetro llamado cursor
. Puedes personalizar esto usando la extensión x-cursor
:
@app.get( "/items", openapi_extra={ # Especifica un nombre diferente para el parámetro del cursor "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token # La respuesta debe incluir el cursor con el mismo nombre }
Si no quieres generar infiniteQueryOptions
para una operación, puedes establecer x-cursor
en False
:
@app.get( "/items", openapi_extra={ # Deshabilita la paginación basada en cursor para este endpoint "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }
Agrupación de operaciones
Los hooks y métodos del cliente generado se organizan automáticamente según los tags OpenAPI en tus endpoints de FastAPI. Esto ayuda a mantener organizadas tus llamadas API 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 estos tags:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Las operaciones de Items se agrupan bajo api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Las operaciones de Users se agrupan bajo api.users const users = useQuery(api.users.list.queryOptions());
// Ejemplo de uso 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}>Añadir ítem</button>
<h2>Usuarios</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
Esta agrupación facilita organizar tus llamadas API y provee mejor autocompletado en tu IDE.
Operaciones agrupadas usando el cliente de 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);
// Cargar datos useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Operaciones de Items agrupadas bajo api.items const itemsData = await api.items.list(); setItems(itemsData);
// Operaciones de Users agrupadas bajo api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error obteniendo datos:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Crear ítem usando el método agrupado const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creando ítem:', error); } };
if (isLoading) { return <div>Cargando...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Añadir ítem</button>
<h2>Usuarios</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
También puedes dividir tu API usando múltiples routers
. Consulta la Documentación de FastAPI para más detalles.
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
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
Luego crea clases de excepción para diferentes escenarios:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
Añadir 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(), )
JSONResponse
acepta un diccionario, así que usamos el método model_dump
de nuestro modelo Pydantic.
Especificar modelos de respuesta
Finalmente, especifica los modelos de respuesta para diferentes códigos de estado en tus definiciones de 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 con ID {item_id} no encontrado") 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="Datos de ítem inválidos", field_errors=["name es requerido"] ) ) return save_item(item)
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();
// Consulta con manejo de errores tipado const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // El error está tipado según las respuestas en tu FastAPI switch (error.status) { case 404: // error.error es un string como se especificó en las respuestas console.error('No encontrado:', error.error); break; case 500: // error.error está tipado como ErrorDetails console.error('Error del servidor:', error.error.message); break; } } });
// Mutación con manejo de errores tipado const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error está tipado como ValidationError console.error('Error de validación:', error.error.message); console.error('Errores de campo:', error.error.field_errors); break; case 403: // error.error es un string como se especificó en las respuestas console.error('Prohibido:', error.error); break; } } });
// Renderizado del componente con manejo de errores if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Contenido del componente */} </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);
// Obtener ítem con manejo de errores useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // El error está tipado según las respuestas en tu FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error es un string como se especificó en las respuestas console.error('No encontrado:', err.error); break; case 500: // err.error está tipado como ErrorDetails console.error('Error del servidor:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Crear ítem con manejo de errores const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error está tipado como ValidationError console.error('Error de validación:', err.error.message); console.error('Errores de campo:', err.error.field_errors); break; case 403: // err.error es un string como se especificó en las respuestas console.error('Prohibido:', err.error); break; } } };
// Renderizado del componente con manejo de errores 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> {/* Contenido del componente */} </div> );}
Al definir respuestas de error en FastAPI, siempre usa el parámetro responses
para especificar el modelo para cada código de estado. Esto asegura que el cliente generado tendrá información de tipos adecuada para el manejo de errores.
Mejores prácticas
Manejar estados de carga
Siempre maneja estados de carga y error 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 está tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error está tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="Ocurrió un error desconocido" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Manejar estados de carga usando el cliente 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 está tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error está tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="Ocurrió un error desconocido" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
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();
// Consulta para obtener ítems const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutación para eliminar ítems con actualizaciones optimistas const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancela cualquier recarga pendiente await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Toma una instantánea del valor anterior const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Actualiza optimistamente al nuevo valor queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Retorna un objeto de contexto con la instantánea return { previousItems }; }, onError: (err, itemId, context) => { // Si la mutación falla, usa el contexto para revertir queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Error al eliminar ítem:', err); }, onSettled: () => { // Siempre recarga después de error o éxito para sincronizar datos queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Error al cargar ítems" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Eliminando...' : 'Eliminar'} </button> </li> ))} </ul> );}
Actualizaciones optimistas usando el cliente directamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Eliminación optimista const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restaura los ítems anteriores en caso de error setItems(previousItems); console.error('Error al eliminar ítem:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Eliminar</button> </li> ))} </ul> );}
Seguridad de tipos
La integración provee seguridad de tipos de extremo a extremo. Tu IDE dará autocompletado completo y verificación de tipos para todas tus llamadas API:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Mutación tipada para crear ítems const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Error de tipo si onSuccess no maneja el tipo de respuesta correcto onSuccess: (data) => { // data está completamente tipado según el esquema de respuesta de tu API console.log(`Ítem creado con ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Error de tipo si la entrada no coincide con el esquema createItem.mutate(data); };
// La UI de error puede usar estrechamiento de tipos para manejar diferentes errores if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error está tipado como CreateItem400Response return ( <FormError message="Entrada inválida" errors={error.error.validationErrors} /> ); case 403: // error.error está tipado como CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error está tipado como CreateItem5XXResponse para 500, 502, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Campos del formulario */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creando...' : 'Crear ítem'} </button> </form> );}
Seguridad de tipos usando el cliente directamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Error de tipo si la entrada no coincide con el esquema await api.createItem(data); } catch (e) { // ✅ El tipo de error incluye todas las posibles respuestas de error const err = e as CreateItemError; switch (err.status) { case 400: // err.error está tipado como CreateItem400Response console.error('Errores de validación:', err.error.validationErrors); break; case 403: // err.error está tipado como CreateItem403Response console.error('No autorizado:', err.error.reason); break; case 500: case 502: // err.error está tipado como CreateItem5XXResponse console.error( 'Error del servidor:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// UI de error puede usar estrechamiento de tipos para manejar diferentes errores if (error) { switch (error.status) { case 400: return ( <FormError message="Entrada inválida" 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 del esquema OpenAPI de tu FastAPI, asegurando que cualquier cambio en tu API se refleje en tu código frontend después de un build.