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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParâmetroTipoPadrãoDescrição
sourceProject Obrigatóriostring-O projeto de origem
targetProject Obrigatóriostring-O projeto de destino para conectar
sourceComponent string-O componente de origem para conectar (nome do componente, caminho relativo à raiz do projeto de origem, ou id do gerador). Use '.' para selecionar explicitamente o projeto como origem.
targetComponent string-O componente de destino para conectar (nome do componente, caminho relativo à raiz do projeto de destino, ou id do gerador). Use '.' para selecionar explicitamente o projeto como destino.
preferInstallDependencies booleantrueSe 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.isLoading) return <div>Loading...</div>;
if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;
}
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: {createItem.error.message}
</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.isLoading) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={items.error.message} />;
}
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 <operation-name>Error é gerado, encapsulando as possíveis respostas de erro definidas no modelo Smithy. Cada erro possui uma propriedade status e error, e ao verificar o valor de status você pode identificar um tipo específico de erro.

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 is typed as CreateItem400Response
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as CreateItem403Response
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
case 502:
// error.error is typed as CreateItem5XXResponse
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: "/items")
@query
operation ListItems {
input: ListItemsInput
output: ListItemsOutput
}

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

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

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

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

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:

@cursor(enabled: false)
operation ListItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
cursor: String
}
output := {
...
}
}

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.

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

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?.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2>
<ul>
{users.data?.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(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
}

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();
// Query with typed error handling
const getItem = useQuery({
...api.getItem.queryOptions({ itemId: '123' }),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 404:
// error.error is typed as ItemNotFoundError
console.error('Not found:', error.error.message);
break;
case 500:
// error.error is typed as InternalServerError
console.error('Server error:', error.error.message);
console.error('Trace ID:', error.error.traceId);
break;
}
}
});
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
switch (error.status) {
case 400:
// error.error is typed as InvalidRequestError
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedError
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
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>
{/* 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 ListItems403Response
return <ErrorMessage message={err.error.reason} />;
case 500:
case 502:
// err.error is typed as ListItems5XXResponse
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data.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';
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(api.listItems.queryKey());
// Optimistically update to the new value
queryClient.setQueryData(
api.listItems.queryKey(),
(old) => old.filter((item) => item.id !== itemId)
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, itemId, 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.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate(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 400:
// error.error is typed as InvalidRequestError
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedError
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerError 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.