Pular para o conteúdo

React para API Smithy

O gerador connection fornece uma maneira de integrar rapidamente seu site React com seu backend de API TypeScript Smithy. Ele configura toda a configuração necessária para conectar-se à sua API Smithy de forma type-safe, incluindo geração de cliente e hooks TanStack Query, suporte à autenticação AWS IAM e Cognito e tratamento adequado de erros.

Antes de usar este gerador, certifique-se de que sua aplicação React tenha:

  1. Um arquivo main.tsx que renderiza sua aplicação
  2. Um backend de API TypeScript Smithy funcional (gerado usando o gerador ts#api com --framework=smithy)
  3. Cognito Auth adicionado via gerador ts#website#auth se estiver conectando uma API que usa autenticação Cognito ou IAM
Exemplo da estrutura main.tsx necessária
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>,
);

Execute este gerador@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
Monte seu comando5

Obrigatório

Obrigatório

Opções do gerador5 opções
sourceProjectObrigatóriostring

O projeto de origem

targetProjectObrigatóriostring

O projeto de destino para conectar

sourceComponentstring

O componente de origem a partir do qual conectar (nome do componente, caminho relativo à raiz do projeto de origem, ou id do gerador). Use '.' para selecionar explicitamente o projeto como origem.

targetComponentstring

O componente de destino ao qual conectar (nome do componente, caminho relativo à raiz do projeto de destino, ou id do gerador). Use '.' para selecionar explicitamente o projeto como destino.

preferInstallDependenciesbooleanPadrão: true

Se deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao executar múltiplos geradores em lote (uma instalação ainda é executada se necessário para que os geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final.

O gerador fará alterações nos seguintes arquivos em sua aplicação React:

  • Directorysrc
    • Directorycomponents
      • <ApiName>Provider.tsx Provider para seu cliente de API
      • QueryClientProvider.tsx Provider do cliente TanStack React Query
      • DirectoryRuntimeConfig/ Componente de configuração de runtime para desenvolvimento local
    • Directoryhooks
      • use<ApiName>.tsx Adiciona um hook para chamar sua API com estado gerenciado pelo TanStack Query
      • use<ApiName>Client.tsx Adiciona um hook para instanciar o cliente de API vanilla que pode chamar sua API.
      • useSigV4.tsx Adiciona um hook para assinar requisições HTTP com SigV4 (se você selecionou autenticação IAM)
  • project.json Um novo target é adicionado ao build que gera um cliente type-safe
  • .gitignore Os arquivos de cliente gerados são ignorados por padrão

O gerador também adicionará um arquivo ao seu modelo Smithy:

  • Directorymodel
    • Directorysrc
      • extensions.smithy Define traits que podem ser usados para customizar o cliente gerado

O gerador também adicionará Runtime Config à infraestrutura do seu site, se ainda não estiver presente, o que garante que a URL da API para sua API Smithy esteja disponível no site e automaticamente configurada pelo hook use<ApiName>.tsx.

No momento do build, um cliente type-safe é gerado a partir da especificação OpenAPI da sua API Smithy. Isso adicionará três novos arquivos à sua aplicação React:

  • Directorysrc
    • Directorygenerated
      • Directory<ApiName>
        • types.gen.ts Tipos gerados a partir das estruturas do modelo Smithy
        • client.gen.ts Cliente type-safe para chamar sua API
        • options-proxy.gen.ts Fornece métodos para criar opções de hooks TanStack Query para interagir com sua API usando TanStack Query

O cliente type-safe gerado pode ser usado para chamar sua API Smithy a partir da sua aplicação React. É recomendado fazer uso do cliente através dos hooks TanStack Query, mas você pode usar o cliente vanilla se preferir.

O gerador fornece um hook use<ApiName> que você pode usar para chamar sua API com TanStack Query.

Você pode usar o método queryOptions para recuperar as opções necessárias para chamar sua API usando o hook useQuery do 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>;
}

O erro é uma união discriminada de objetos { status, error } em vez de um Error, então não há message de nível superior para renderizar. Restrinja em status para ler um corpo de resposta específico — veja Tratamento de Erros abaixo.

Clique aqui para um exemplo usando o cliente vanilla diretamente.

Os hooks gerados incluem suporte para mutations usando o hook useMutation do TanStack Query. Isso fornece uma maneira limpa de lidar com operações de criação, atualização e exclusão com estados de carregamento, tratamento de erros e atualizações otimistas.

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>
);
}

Você também pode adicionar callbacks para diferentes estados de mutation:

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({}) });
}
});
Clique aqui para um exemplo usando o cliente diretamente.

Para endpoints que aceitam um parâmetro cursor como entrada, os hooks gerados fornecem suporte a infinite queries usando o hook useInfiniteQuery do TanStack Query. Isso facilita a implementação de funcionalidades “carregar mais” ou 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, // 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>
);
}

Os hooks gerados lidam automaticamente com a paginação baseada em cursor se sua API suportar. O valor nextCursor é extraído da resposta e usado para buscar a próxima página.

Clique aqui para ver um exemplo usando o cliente diretamente.

A integração inclui tratamento de erros com respostas tipadas. Um tipo <OperationName>Error é gerado que é uma união de um membro <OperationName><StatusCode>Error por status de erro que a operação modela. Cada membro tem uma propriedade status e uma propriedade error, então ao verificar status você restringe error ao payload daquele status.

O exemplo abaixo assume que CreateItem modela as estruturas de erro definidas mais abaixo nesta página — um InvalidRequestError 422, um UnauthorizedError 403 e um InternalServerError 500. Apenas os status que seu modelo declara aparecem na união, então um case para um status que você não modelou é um erro de compilação.

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>;
}
Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

Uma seleção de traits Smithy são adicionadas ao projeto Smithy model em extensions.smithy que você pode usar para personalizar o cliente gerado.

Por padrão, operações na sua API Smithy que usam os métodos HTTP PUT, POST, PATCH e DELETE são consideradas mutations, e as demais são queries.

Você pode alterar este comportamento usando os traits @query e @mutation Smithy que são adicionados ao seu projeto de modelo em extensions.smithy.

Aplique o trait @query à sua operação Smithy para forçá-la a ser tratada como query:

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

O hook gerado fornecerá queryOptions mesmo usando o método HTTP POST:

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

Aplique o trait @mutation à sua operação Smithy para forçá-la a ser tratada como mutation:

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

O hook gerado fornecerá mutationOptions mesmo usando o método HTTP GET:

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

Por padrão, os hooks gerados assumem paginação baseada em cursor com um parâmetro chamado cursor. Você pode personalizar este comportamento usando o trait @cursor adicionado ao seu projeto de modelo em extensions.smithy.

Aplique o trait @cursor com inputToken para alterar o nome do parâmetro de entrada usado para o token de paginação:

@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 então pagina em nextToken:

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

Se não quiser gerar infiniteQueryOptions para uma operação que tem um parâmetro de entrada chamado cursor, você pode desativar a paginação baseada em 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 então fornece apenas queryOptions, queryKey e queryFilter.

Os hooks e métodos do cliente gerado são organizados automaticamente com base no trait @tags em suas operações Smithy. Operações com as mesmas tags são agrupadas, o que ajuda a manter suas chamadas de API organizadas e fornece melhor completação de código em sua IDE.

Dentro de um grupo, cada operação mantém seu próprio nome em camelCase, então uma operação chamada ListItems com tag "items" é acessada em api.items.listItems — não api.items.list.

Por exemplo, com 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
}
}

Os hooks gerados serão agrupados por tags:

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>
);
}

Este agrupamento facilita a organização das chamadas de API e fornece melhor completação de código em sua IDE.

Clique aqui para ver um exemplo usando o cliente diretamente.

Você pode personalizar respostas de erro em sua API Smithy definindo estruturas de erro personalizadas em seu modelo Smithy. O cliente gerado lidará automaticamente com esses tipos de erro personalizados.

Defina suas estruturas de erro em seu 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
}

Especifique quais erros suas operações podem retornar:

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
}

O cliente gerado lidará automaticamente com esses tipos de erro personalizados, permitindo verificação de tipos e tratamento de diferentes respostas de erro:

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>
);
}
Clique aqui para ver um exemplo usando o cliente diretamente.

Sempre trate estados de carregamento e erro para uma melhor experiência do usuário:

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>
);
}
Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

Implemente atualizações otimistas para uma melhor experiência do usuário:

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>
);
}
Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

A integração fornece segurança de tipos completa de ponta a ponta. Sua IDE fornecerá autocompletar e verificação de tipos para todas as chamadas de 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>
);
}
Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

Os tipos são gerados automaticamente a partir do schema OpenAPI da sua API Smithy, garantindo que quaisquer alterações na API sejam refletidas no código frontend após um build.

Se sua API Smithy usa autenticação Custom (Lambda Authorizer), você precisará editar o provedor do cliente gerado para adicionar os cabeçalhos de autorização que seu autorizador espera. Procure a configuração fetch no <ApiName>Provider.tsx gerado e adicione seu token ou chave de API aos cabeçalhos da requisição.