Saltearse al contenido

React a FastAPI

El generador api-connection proporciona una forma rápida de integrar tu aplicación React con tu backend FastAPI. Configura toda la configuración necesaria para conectarse a tus backends FastAPI de manera tipada, incluyendo generación de clientes y hooks de TanStack Query, soporte para autenticación AWS IAM y manejo adecuado de errores.

Requisitos previos

Antes de usar este generador, asegúrate que tu aplicación React tenga:

  1. Un archivo main.tsx que renderice tu aplicación
  2. Un backend FastAPI funcional (generado usando el generador FastAPI)
Ejemplo de estructura requerida para 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>,
);

Uso

Ejecutar el generador

  1. Instale el Nx Console VSCode Plugin si aún no lo ha hecho
  2. Abra la consola Nx en VSCode
  3. Haga clic en Generate (UI) en la sección "Common Nx Commands"
  4. Busque @aws/nx-plugin - api-connection
  5. Complete los parámetros requeridos
    • Haga clic en Generate

    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
    auth string IAM Authentication strategy (choose from IAM or None)

    Salida del generador

    El generador modificará los siguientes archivos en tu proyecto FastAPI:

    • Directoryscripts
      • generate_open_api.py Añade un script que genera la especificación OpenAPI para tu API
    • project.json Se añade un nuevo target al build que invoca el script de generación

    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 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 el cliente tipado
    • .gitignore Los archivos generados del cliente se ignoran por defecto

    El generador también añadirá Runtime Config a la infraestructura de tu sitio web si no existe, asegurando que la URL de la API para 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 que interactúan con tu API

    Usando el código generado

    El cliente tipado generado puede usarse para llamar a tu FastAPI desde la aplicación React. Se recomienda usar los hooks de TanStack Query, pero también puedes usar el cliente vanilla si prefieres.

    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 la 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>;
    }
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    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();
    // Crear 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() });
    }
    });
    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    Paginación con consultas infinitas

    Para endpoints que aceptan un parámetro cursor, 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>
    {/* Aplanar 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.

    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    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 determinar el 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>;
    }
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    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:

    1. Se envía la petición HTTP para iniciar el stream

      • isLoading es true
      • fetchStatus es 'fetching'
      • data es undefined
    2. Se recibe el primer fragmento del stream

      • isLoading pasa a false
      • fetchStatus sigue siendo 'fetching'
      • data se convierte en un array con el primer fragmento
    3. Se reciben fragmentos subsiguientes

      • isLoading sigue siendo false
      • fetchStatus sigue siendo 'fetching'
      • data se actualiza con cada nuevo fragmento recibido
    4. El stream finaliza

      • isLoading sigue siendo false
      • fetchStatus pasa a 'idle'
      • data es un array con todos los fragmentos recibidos
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    Personalizando el 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 personalizadas
    const 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 generados se organizan automáticamente según los tags OpenAPI en tus endpoints FastAPI. Esto ayuda a mantener organizadas las llamadas API y facilita encontrar operaciones relacionadas.

    Por ejemplo:

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

    Los hooks generados se agruparán según 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>Ítems</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.

    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    Errores

    Puedes personalizar las respuestas de error en tu FastAPI definiendo clases de excepción personalizadas, manejadores de excepción 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:

    models.py
    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:

    exceptions.py
    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 excepción

    Registra manejadores de excepción para convertir tus excepciones en respuestas 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(),
    )

    Especificar modelos de respuesta

    Finalmente, especifica los modelos de respuesta para diferentes códigos de estado en tus endpoints:

    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 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 tipar 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>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    Mejores prácticas

    Manejar estados de carga

    Siempre maneja los 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="Error desconocido" />;
    }
    }
    return (
    <ul>
    {items.data.map((item) => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    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) => {
    // Cancelar cualquier recarga pendiente
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Tomar instantánea del valor previo
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Actualizar optimistamente al nuevo valor
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Retornar 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 eliminando ítem:', err);
    },
    onSettled: () => {
    // Siempre recargar 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 cargando í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>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    Seguridad de tipos

    La integración provee seguridad de tipos de extremo a extremo. Tu IDE proveerá autocompletado 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 el callback onSuccess no maneja el tipo de respuesta correcto
    onSuccess: (data) => {
    // data está completamente tipado según el schema 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 schema
    createItem.mutate(data);
    };
    // La UI de errores puede usar narrowing 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>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    Los tipos se generan automáticamente del schema OpenAPI de tu FastAPI, asegurando que cualquier cambio en tu API se refleje en tu código frontend después de un build.