Saltearse al contenido

Reaccionar a la API de Smithy

El generador api-connection proporciona una forma rápida de integrar tu sitio web React con tu backend de API Smithy TypeScript. Configura toda la configuración necesaria para conectarse a tu API Smithy 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.

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 de API Smithy TypeScript funcional (generado usando el generador ts#smithy-api)
  3. Auth con Cognito añadido mediante el generador ts#react-website-auth si conectas una API que use autenticación Cognito o IAM
Ejemplo de la 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>,
);
  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
    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

    El generador realizará cambios en los siguientes archivos de tu aplicación React:

    • Directorysrc
      • Directorycomponents
        • <ApiName>Provider.tsx Proveedor para el cliente de tu API
        • QueryClientProvider.tsx Proveedor del cliente TanStack React Query
        • DirectoryRuntimeConfig/ Componente de configuración en tiempo de ejecución para desarrollo local
      • 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á un archivo a tu modelo Smithy:

    • Directorymodel
      • Directorysrc
        • extensions.smithy Define traits que pueden usarse para personalizar el cliente generado

    Además, el generador añadirá Runtime Config a tu infraestructura de website si no existe, asegurando que la URL de la API para tu API Smithy esté disponible en el website y configurada automáticamente por el hook use<ApiName>.tsx.

    En tiempo de build, se genera un cliente tipado a partir de la especificación OpenAPI de tu API Smithy. Esto añadirá tres nuevos archivos a tu aplicación React:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Tipos generados de las estructuras del modelo Smithy
          • 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

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

    El generador provee un hook use<ApiName> que puedes usar para llamar a tu API con TanStack Query.

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

    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.

    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 para retornar
    // el parámetro que debe pasarse 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.

    La integración incluye manejo de errores integrado con respuestas de error tipadas. Se genera un tipo <operation-name>Error que encapsula las posibles respuestas de error definidas en el modelo Smithy. 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>
    </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>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Crear ítem</button>;
    }
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    Se añaden varios traits Smithy a tu proyecto Smithy model en extensions.smithy que puedes usar para personalizar el cliente generado.

    Por defecto, las operaciones en tu API Smithy 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 los traits Smithy @query y @mutation que se añaden a tu proyecto de modelo en extensions.smithy.

    Aplica el trait @query a tu operación Smithy para forzar que se trate como consulta:

    @http(method: "POST", uri: "/items")
    @query
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }

    El hook generado proveerá queryOptions aunque use el método HTTP POST:

    const items = useQuery(api.listItems.queryOptions());

    Aplica el trait @mutation a tu operación Smithy para forzar que se trate como mutación:

    @http(method: "GET", uri: "/start-processing")
    @mutation
    operation StartProcessing {
    input: StartProcessingInput
    output: StartProcessingOutput
    }

    El hook generado proveerá mutationOptions aunque use el método HTTP GET:

    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Por defecto, los hooks generados asumen paginación basada en cursor con un parámetro llamado cursor. Puedes personalizar este comportamiento usando el trait @cursor que se añade a tu proyecto de modelo en extensions.smithy.

    Aplica el trait @cursor con inputToken para cambiar el nombre del parámetro de entrada usado para el token de paginación:

    @http(method: "GET", uri: "/items")
    @cursor(inputToken: "nextToken")
    operation ListItems {
    input := {
    nextToken: String
    limit: Integer
    }
    output := {
    items: ItemList
    nextToken: String
    }
    }

    Si no quieres generar infiniteQueryOptions para una operación que tiene un parámetro de entrada llamado cursor, puedes deshabilitar la paginación basada en cursor:

    @cursor(enabled: false)
    operation ListItems {
    input := {
    // El parámetro de entrada llamado 'cursor' hará que esta operación se trate como paginada por defecto
    cursor: String
    }
    output := {
    ...
    }
    }

    Los hooks y métodos del cliente generado se organizan automáticamente basados en el trait @tags en tus operaciones Smithy. Las operaciones con los mismos tags se agrupan juntas, lo que ayuda a mantener organizadas tus llamadas API y provee mejor autocompletado en tu IDE.

    Por ejemplo, con este modelo Smithy:

    service MyService {
    operations: [ListItems, CreateItem, ListUsers, CreateUser]
    }
    @tags(["items"])
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }
    @tags(["items"])
    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    }
    @tags(["users"])
    operation ListUsers {
    input: ListUsersInput
    output: ListUsersOutput
    }
    @tags(["users"])
    operation CreateUser {
    input: CreateUserInput
    output: CreateUserOutput
    }

    Los hooks generados se agruparán por 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.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // Las operaciones de users se agrupan bajo api.users
    const users = useQuery(api.users.listUsers.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.

    Puedes personalizar las respuestas de error en tu API Smithy definiendo estructuras de error personalizadas en tu modelo Smithy. El cliente generado manejará automáticamente estos tipos de error.

    Definiendo estructuras de error personalizadas

    Sección titulada «Definiendo estructuras de error personalizadas»

    Define tus estructuras de error en tu modelo Smithy:

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    fieldErrors: FieldErrorList
    }
    @error("client")
    @httpError(403)
    structure UnauthorizedError {
    @required
    reason: String
    }
    @error("server")
    @httpError(500)
    structure InternalServerError {
    @required
    message: String
    traceId: String
    }
    list FieldErrorList {
    member: FieldError
    }
    structure FieldError {
    @required
    field: String
    @required
    message: String
    }

    Especifica qué errores pueden retornar tus operaciones:

    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    errors: [
    InvalidRequestError
    UnauthorizedError
    InternalServerError
    ]
    }
    operation GetItem {
    input: GetItemInput
    output: GetItemOutput
    errors: [
    ItemNotFoundError
    InternalServerError
    ]
    }
    @error("client")
    @httpError(404)
    structure ItemNotFoundError {
    @required
    message: String
    }

    Usando tipos de error personalizados en React

    Sección titulada «Usando tipos de error personalizados en React»

    El cliente generado manejará automáticamente estos tipos de error, 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 los errores en tu modelo Smithy
    switch (error.status) {
    case 404:
    // error.error está tipado como ItemNotFoundError
    console.error('No encontrado:', error.error.message);
    break;
    case 500:
    // error.error está tipado como InternalServerError
    console.error('Error del servidor:', error.error.message);
    console.error('Trace ID:', error.error.traceId);
    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 InvalidRequestError
    console.error('Error de validación:', error.error.message);
    console.error('Errores de campo:', error.error.fieldErrors);
    break;
    case 403:
    // error.error está tipado como UnauthorizedError
    console.error('No autorizado:', error.error.reason);
    break;
    }
    }
    });
    // Renderizado del componente con manejo de errores
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error.message} />;
    } else if (getItem.error.status === 500) {
    return <ErrorMessage message={getItem.error.error.message} />;
    }
    }
    return (
    <div>
    {/* Contenido del componente */}
    </div>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente directamente.

    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}
    />
    );
    default:
    return <ErrorMessage message="Ocurrió un 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.

    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 refetch pendiente
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // Snapshot del valor anterior
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // Actualización optimista al nuevo valor
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // Retornar un objeto de contexto con el snapshot
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // Si falla la mutación, usa el contexto de onMutate para revertir
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('Error al eliminar ítem:', err);
    },
    onSettled: () => {
    // Siempre refetch 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>
    );
    }
    Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

    La integración provee seguridad de tipos completa 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 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);
    };
    // 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 InvalidRequestError
    return (
    <FormError
    message="Entrada inválida"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error está tipado como UnauthorizedError
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error está tipado como InternalServerError para 500, 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 vanilla directamente.

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