Ir al contenido

React a API Smithy

El generador connection proporciona una forma de integrar rápidamente 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 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.

Antes de usar este generador, asegúrate de 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#api con --framework=smithy)
  3. Cognito Auth agregado a través del generador ts#website#auth si 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>,
);

Ejecute este generador@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
Construya su comando5

Requerido

Requerido

Opciones del generador5 opciones
sourceProjectRequeridostring

El proyecto de origen

targetProjectRequeridostring

El proyecto de destino al que conectar

sourceComponentstring

El componente de origen desde el cual conectar (nombre del componente, ruta relativa a la raíz del proyecto de origen, o id del generador). Usa '.' para seleccionar explícitamente el proyecto como origen.

targetComponentstring

El componente de destino al cual conectar (nombre del componente, ruta relativa a la raíz del proyecto de destino, o id del generador). Usa '.' para seleccionar explícitamente el proyecto como destino.

preferInstallDependenciesbooleanPredeterminado: 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.

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

  • Directoriosrc
    • Directoriocomponents
      • <ApiName>Provider.tsx Provider para tu cliente API
      • QueryClientProvider.tsx Proveedor de cliente TanStack React Query
      • DirectorioRuntimeConfig/ Componente de configuración en tiempo de ejecución para desarrollo local
    • Directoriohooks
      • use<ApiName>.tsx Agrega un hook para llamar a tu API con estado administrado 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 se ignoran por defecto

El generador también agregará un archivo a tu modelo Smithy:

  • Directoriomodel
    • Directoriosrc
      • extensions.smithy Define traits que se pueden usar para personalizar el cliente generado

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 API Smithy esté disponible en el sitio web y se configure automáticamente mediante el hook use<ApiName>.tsx.

En tiempo de compilación, se genera un cliente seguro en cuanto a tipos a partir de la especificación OpenAPI de tu API Smithy. Esto agregará tres nuevos archivos a tu aplicación React:

  • Directoriosrc
    • Directoriogenerated
      • Directorio<ApiName>
        • types.gen.ts Tipos generados a partir de las estructuras del modelo Smithy
        • 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

El cliente seguro en cuanto a tipos generado se puede usar para llamar a tu API Smithy desde tu aplicación React. Se recomienda hacer uso del cliente a través de los hooks de TanStack Query, pero puedes usar el cliente vanilla si lo prefieres.

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

Puedes usar el método queryOptions para recuperar 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.isPending) return <div>Loading...</div>;
if (item.isError) return <div>Error: {JSON.stringify(item.error.error)}</div>;
return <div>Item: {item.data.name}</div>;
}

El error es una unión discriminada de objetos { status, error } en lugar de un Error, por lo que no hay un message de nivel superior para renderizar. Reduce en status para leer un cuerpo de respuesta específico — consulta Manejo de errores a continuación.

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 proporciona 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: {JSON.stringify(createItem.error.error)}
</div>
)}
</form>
);
}

También puedes agregar callbacks para diferentes estados de 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({}) });
}
});
Haz clic aquí para ver un ejemplo usando el cliente directamente.

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.isPending) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={JSON.stringify(items.error.error)} />;
}
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 admite. El valor nextCursor se extrae de la respuesta y se usa para obtener la siguiente página.

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

La integración incluye manejo de errores incorporado con respuestas de error tipadas. Se genera un tipo <OperationName>Error que es una unión de un miembro <OperationName><StatusCode>Error por cada estado de error que modela la operación. Cada miembro tiene una propiedad status y una propiedad error, por lo que cambiar en status reduce error a la carga útil de ese estado.

El ejemplo a continuación asume que CreateItem modela las estructuras de error definidas más adelante en esta página — un InvalidRequestError de 422, un UnauthorizedError de 403 y un InternalServerError de 500. Solo los estados que declara tu modelo aparecen en la unión, por lo que un case para un estado que no has modelado es un error de compilación.

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 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
// error.error is typed as InternalServerErrorResponseContent
return (
<div>
<h2>Server error:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</button>;
}
Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

Se agrega una selección de traits de Smithy a tu proyecto model de Smithy objetivo 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 todas las demás se consideran consultas.

Puedes cambiar este comportamiento usando los traits de Smithy @query y @mutation que se agregan a tu proyecto de modelo en extensions.smithy.

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

@http(method: "POST", uri: "/search-items")
@query
operation SearchItems {
input: SearchItemsInput
output: SearchItemsOutput
}

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

const items = useQuery(api.searchItems.queryOptions({ term: 'widget' }));

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

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

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

const startProcessing = useMutation(api.startProcessing.mutationOptions());
startProcessing.mutate({ id: 'some-id' });

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 agrega 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: "/paged-items")
@cursor(inputToken: "nextToken")
operation ListPagedItems {
input := {
@httpQuery("nextToken")
nextToken: String
@httpQuery("limit")
limit: Integer
}
output := {
@required
items: ItemList
nextToken: String
}
}

infiniteQueryOptions entonces pagina en nextToken:

const items = useInfiniteQuery({
...api.listPagedItems.infiniteQueryOptions(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextToken || undefined },
),
});

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

@http(method: "GET", uri: "/unpaged-items")
@cursor(enabled: false)
operation ListUnpagedItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
@httpQuery("cursor")
cursor: String
}
output := {
@required
items: ItemList
}
}

api.listUnpagedItems entonces solo proporciona queryOptions, queryKey y queryFilter.

Los hooks y métodos de cliente generados se organizan automáticamente según el trait @tags en tus operaciones Smithy. Las operaciones con las mismas etiquetas se agrupan juntas, lo que ayuda a mantener tus llamadas API organizadas y proporciona una mejor finalización de código en tu IDE.

Dentro de un grupo, cada operación mantiene su propio nombre en camelCase, por lo que una operación llamada ListItems etiquetada como "items" se alcanza en api.items.listItems — no en api.items.list.

Por ejemplo, con este modelo Smithy:

service MyService {
operations: [ListItems, CreateItem, ListUsers, CreateUser]
}
@tags(["items"])
@http(method: "GET", uri: "/items")
@readonly
operation ListItems {
input := {}
output := {
@required
items: ItemList
}
}
@tags(["items"])
@http(method: "POST", uri: "/items")
operation CreateItem {
input := {
@required
name: String
}
output := {
@required
id: String
}
}
@tags(["users"])
@http(method: "GET", uri: "/users")
@readonly
operation ListUsers {
input := {}
output := {
@required
users: UserList
}
}
@tags(["users"])
@http(method: "POST", uri: "/users")
operation CreateUser {
input := {
@required
name: String
}
output := {
@required
id: String
}
}

Los hooks generados se agruparán por 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.listItems.queryOptions());
const createItem = useMutation(api.items.createItem.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.listUsers.queryOptions());
// Usage example
const handleCreateItem = () => {
createItem.mutate({ name: 'New Item' });
};
return (
<div>
<h2>Items</h2>
<ul>
{items.data?.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2>
<ul>
{users.data?.users.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.

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 personalizados.

Definición de estructuras de error personalizadas

Sección titulada «Definición de estructuras de error personalizadas»

Define tus estructuras de error en tu modelo Smithy:

@error("client")
@httpError(422)
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 devolver 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
}

Uso de tipos de error personalizados en React

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

El cliente generado manejará automáticamente estos tipos de error personalizados, lo que te permite verificar tipos y manejar diferentes respuestas de error:

import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() {
const api = useMyApi();
const getItem = useQuery(api.getItem.queryOptions({ itemId: '123' }));
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
switch (getItem.error.status) {
case 404:
// error.error is typed as ItemNotFoundErrorResponseContent
return <NotFoundMessage message={getItem.error.error.message} />;
case 500:
// error.error is typed as InternalServerErrorResponseContent
return <ErrorMessage message={getItem.error.error.message} />;
}
}
return (
<div>
{/* Component content */}
</div>
);
}
Haz clic aquí para ver un ejemplo usando el cliente directamente.

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 is typed as UnauthorizedErrorResponseContent
return <ErrorMessage message={err.error.reason} />;
case 500:
// err.error is typed as InternalServerErrorResponseContent
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data?.items.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';
import type { ListItemsResponseContent } from '../generated/my-api/types.gen';
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<ListItemsResponseContent>(
api.listItems.queryKey({}),
);
// Optimistically update to the new value
queryClient.setQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
(old) =>
old && {
...old,
items: old.items.filter((item) => item.id !== itemId),
},
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, _input, 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?.items.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate({ itemId: item.id })}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
);
}
Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

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 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerErrorResponseContent for 500, 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>
);
}
Haz clic aquí para ver un ejemplo usando el cliente vanilla directamente.

Los tipos se generan automáticamente a partir del esquema OpenAPI de tu API Smithy, lo que garantiza que cualquier cambio en tu API se refleje en tu código frontend después de una compilación.

Si tu API Smithy usa autenticación Custom (Lambda Authorizer), deberá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 solicitud.