Reagir para API Smithy
O gerador api-connection
fornece uma maneira rápida de integrar seu site React com seu backend de API TypeScript Smithy. Ele configura toda a configuração necessária para conectar-se à API Smithy de forma tipada, incluindo geração de clientes e hooks do TanStack Query, suporte a autenticação AWS IAM e Cognito, e tratamento adequado de erros.
Pré-requisitos
Seção intitulada “Pré-requisitos”Antes de usar este gerador, certifique-se que sua aplicação React possui:
- Um arquivo
main.tsx
que renderiza sua aplicação - Um backend de API TypeScript Smithy funcional (gerado usando o gerador
ts#smithy-api
) - Autenticação Cognito adicionada via gerador
ts#react-website-auth
se conectando a uma API que usa autenticação Cognito ou IAM
Exemplo da estrutura necessária do 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>,);
Executar o Gerador
Seção intitulada “Executar o Gerador”- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - api-connection
- Preencha os parâmetros obrigatórios
- Clique em
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
Parâmetro | Tipo | Padrão | Descrição |
---|---|---|---|
sourceProject Obrigatório | string | - | The source project which will call the API |
targetProject Obrigatório | string | - | The target project containing your API |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador fará alterações nos seguintes arquivos de sua aplicação React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Provedor do cliente da API
- QueryClientProvider.tsx Provedor 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 vanilla da API
- useSigV4.tsx Adiciona um hook para assinar requisições HTTP com SigV4 (se selecionou autenticação IAM)
- project.json Um novo target é adicionado ao build para gerar um cliente tipado
- .gitignore Os arquivos do cliente gerado 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 personalizar o cliente gerado
O gerador também adicionará Runtime Config à infraestrutura do seu site se não estiver presente, garantindo que a URL da API Smithy esteja disponível no site e configurada automaticamente pelo hook use<ApiName>.tsx
.
Geração de Código
Seção intitulada “Geração de Código”Durante o build, um cliente tipado é 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 tipado para chamar sua API
- options-proxy.gen.ts Fornece métodos para criar opções de hooks do TanStack Query
Por padrão, o cliente gerado é ignorado do controle de versão. Se preferir incluí-lo, você pode remover a entrada do arquivo .gitignore
da sua aplicação React, mas note que quaisquer alterações manuais nos arquivos .gen.ts
serão sobrescritas ao construir o projeto.
Usando o Código Gerado
Seção intitulada “Usando o Código Gerado”O cliente tipado gerado pode ser usado para chamar sua API Smithy a partir da aplicação React. Recomenda-se usar os hooks do TanStack Query, mas você pode usar o cliente vanilla se preferir.
Sempre que fizer alterações no modelo Smithy da API, você precisa reconstruir seu projeto para que as mudanças sejam refletidas no cliente gerado. Por exemplo:
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
Se estiver trabalhando ativamente na aplicação React e na API Smithy simultaneamente, use o target serve-local
da aplicação React que regenerará automaticamente o cliente quando a API mudar, além de recarregar automaticamente o site e o servidor local da API Smithy:
pnpm nx run <WebsiteProject>:serve-local
yarn nx run <WebsiteProject>:serve-local
npx nx run <WebsiteProject>:serve-local
bunx nx run <WebsiteProject>:serve-local
Para controle mais granular, você pode usar o target watch-generate:<ApiName>-client
da aplicação React para regenerar o cliente sempre que houver mudanças na API:
pnpm nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
yarn nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
npx nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
bunx nx run <WebsiteProject>:"watch-generate:<ApiName>-client"
Usando o Hook da API
Seção intitulada “Usando o Hook da API”O gerador fornece um hook use<ApiName>
que você pode usar para chamar sua API com TanStack Query.
Consultas
Seção intitulada “Consultas”Você pode usar o método queryOptions
para obter 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>Carregando...</div>; if (item.isError) return <div>Erro: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}
Usando o cliente da API diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Carregando...</div>; if (error) return <div>Erro: {error.message}</div>;
return <div>Item: {item.name}</div>;}
Mutations
Seção intitulada “Mutations”Os hooks gerados incluem suporte a 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(); // Cria uma mutation usando as opções geradas const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'Novo Item', description: 'Um novo item' }); };
return ( <form onSubmit={handleSubmit}> {/* Campos do formulário */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Criando...' : 'Criar Item'} </button>
{createItem.isSuccess && ( <div className="success"> Item criado com ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Erro: {createItem.error.message} </div> )} </form> );}
Você também pode adicionar callbacks para diferentes estados da mutation:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // Executa quando a mutation tem sucesso console.log('Item criado:', data); // Você pode navegar para o novo item navigate(`/items/${data.id}`); }, onError: (error) => { // Executa quando a mutation falha console.error('Falha ao criar item:', error); }, onSettled: () => { // Executa quando a mutation completa (sucesso ou erro) // Bom lugar para invalidar queries afetadas queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});
Mutations usando o cliente da API diretamente
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'Novo Item', description: 'Um novo item' }); setCreatedItem(newItem); // Você pode navegar para o novo item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Falha ao criar item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Campos do formulário */} <button type="submit" disabled={isLoading} > {isLoading ? 'Criando...' : 'Criar Item'} </button>
{createdItem && ( <div className="success"> Item criado com ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Erro: {error.message} </div> )} </form> );}
Paginação com Infinite Queries
Seção intitulada “Paginação com Infinite Queries”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, // Número de itens por página }, { // Defina uma função getNextPageParam para retornar // o parâmetro que deve ser passado como 'cursor' para // a 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> {/* Achata o array de páginas para renderizar todos os itens */} <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 ? 'Carregando mais...' : items.hasNextPage ? 'Carregar Mais' : 'Sem mais itens'} </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.
Se sua API paginada usa um parâmetro de paginação com nome diferente de cursor
, você pode personalizar usando a extensão OpenAPI x-cursor
.
Paginação usando o cliente da API diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Busca dados iniciais useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Função para carregar mais itens const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Carregando mais...' : nextCursor ? 'Carregar Mais' : 'Sem mais itens'} </button> </div> );}
Tratamento de Erros
Seção intitulada “Tratamento de Erros”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: 'Novo Item' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error é tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error é tipado como CreateItem403Response return ( <div> <h2>Não autorizado:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error é tipado como CreateItem5XXResponse return ( <div> <h2>Erro no servidor:</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Criar Item</button>;}
Tratamento de erros usando o cliente da API diretamente
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'Novo Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error é tipado como CreateItem400Response return ( <div> <h2>Entrada inválida:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error é tipado como CreateItem403Response return ( <div> <h2>Não autorizado:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error é tipado como CreateItem5XXResponse return ( <div> <h2>Erro no servidor:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Criar Item</button>;}
Personalizando o Código Gerado
Seção intitulada “Personalizando o Código Gerado”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.
Se não precisar personalizar o cliente gerado, você pode excluir extensions.smithy
com segurança
Queries e Mutations
Seção intitulada “Queries e Mutations”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
.
Estes mapeiam para extensões OpenAPI usando o trait @specificationExtension
, que nosso gerador de código interpreta ao gerar o cliente a partir da especificação OpenAPI.
Aplique o trait @query
à sua operação Smithy para forçá-la a ser tratada como query:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}
O hook gerado fornecerá queryOptions
mesmo usando o método HTTP POST
:
const items = useQuery(api.listItems.queryOptions());
@mutation
Seção intitulada “@mutation”Aplique o trait @mutation
à sua operação Smithy para forçá-la a ser tratada como mutation:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}
O hook gerado fornecerá mutationOptions
mesmo usando o método HTTP GET
:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
Cursor de Paginação Personalizado
Seção intitulada “Cursor de Paginação Personalizado”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 := { // Parâmetro de entrada chamado 'cursor' faria esta operação ser tratada como paginada por padrão cursor: String } output := { ... }}
Agrupamento de Operações
Seção intitulada “Agrupamento de Operações”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();
// Operações de Items são agrupadas em api.items const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Operações de Users são agrupadas em api.users const users = useQuery(api.users.listUsers.queryOptions());
// Exemplo de uso const handleCreateItem = () => { createItem.mutate({ name: 'Novo Item' }); };
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Adicionar Item</button>
<h2>Usuários</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.
Operações agrupadas usando o cliente da API diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Carrega dados useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Operações de Items são agrupadas em api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Operações de Users são agrupadas em api.users const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('Erro ao buscar dados:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Cria item usando o método agrupado const newItem = await api.items.createItem({ name: 'Novo Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Erro ao criar item:', error); } };
if (isLoading) { return <div>Carregando...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Adicionar Item</button>
<h2>Usuários</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
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.
Definindo Estruturas de Erro Personalizadas
Seção intitulada “Definindo Estruturas de Erro Personalizadas”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}
Adicionando Erros a Operações
Seção intitulada “Adicionando Erros a Operações”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}
Usando Tipos de Erro Personalizados em React
Seção intitulada “Usando Tipos de Erro Personalizados em React”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 com tratamento de erro tipado const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error é tipado com base nos erros do modelo Smithy switch (error.status) { case 404: // error.error é tipado como ItemNotFoundError console.error('Não encontrado:', error.error.message); break; case 500: // error.error é tipado como InternalServerError console.error('Erro no servidor:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// Mutation com tratamento de erro tipado const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error é tipado como InvalidRequestError console.error('Erro de validação:', error.error.message); console.error('Erros de campo:', error.error.fieldErrors); break; case 403: // error.error é tipado como UnauthorizedError console.error('Não autorizado:', error.error.reason); break; } } });
// Renderização do componente com tratamento de erro 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> {/* Conteúdo do componente */} </div> );}
Tratando erros personalizados com o cliente diretamente
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Busca item com tratamento de erro useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error é tipado com base nos erros do modelo Smithy const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error é tipado como ItemNotFoundError console.error('Não encontrado:', err.error.message); break; case 500: // err.error é tipado como InternalServerError console.error('Erro no servidor:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Cria item com tratamento de erro const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error é tipado como InvalidRequestError console.error('Erro de validação:', err.error.message); console.error('Erros de campo:', err.error.fieldErrors); break; case 403: // err.error é tipado como UnauthorizedError console.error('Não autorizado:', err.error.reason); break; } } };
// Renderização do componente com tratamento de erro if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error.message} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Conteúdo do componente */} </div> );}
Ao definir estruturas de erro em Smithy, sempre use os traits @error
e @httpError
para especificar o tipo de erro e código de status HTTP. Isso garante que o cliente gerado terá informações de tipo adequadas para tratamento de erros.
Melhores Práticas
Seção intitulada “Melhores Práticas”Lidar com Estados de Carregamento
Seção intitulada “Lidar com Estados de Carregamento”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 é tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error é tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Ocorreu um erro desconhecido" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Lidar com estados de carregamento usando o cliente da API diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; switch (err.status) { case 403: // err.error é tipado como ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error é tipado como ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="Ocorreu um erro desconhecido" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}
Atualizações Otimistas
Seção intitulada “Atualizações Otimistas”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 para buscar items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation para excluir items com atualizações otimistas const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancela qualquer refetch pendente await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Captura o valor anterior const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Atualiza otimistamente para o novo valor queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Retorna um objeto de contexto com o snapshot return { previousItems }; }, onError: (err, itemId, context) => { // Se a mutation falhar, usa o contexto para reverter queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Falha ao excluir item:', err); }, onSettled: () => { // Sempre refetch após erro ou sucesso para sincronizar dados queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Falha ao carregar items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Excluindo...' : 'Excluir'} </button> </li> ))} </ul> );}
Atualizações otimistas usando o cliente da API diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Remove o item otimistamente const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restaura os items anteriores em caso de erro setItems(previousItems); console.error('Falha ao excluir item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Excluir</button> </li> ))} </ul> );}
Segurança de Tipos
Seção intitulada “Segurança de Tipos”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();
// Mutation tipada para criar items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Erro de tipo se o callback onSuccess não lidar com o tipo de resposta correto onSuccess: (data) => { // data é totalmente tipado com base no schema de resposta da API console.log(`Item criado com ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Erro de tipo se a entrada não corresponder ao schema createItem.mutate(data); };
// UI de erro pode usar narrowing de tipos para lidar com diferentes erros if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error é tipado como InvalidRequestError return ( <FormError message="Entrada inválida" errors={error.error.fieldErrors} /> ); case 403: // error.error é tipado como UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error é tipado como InternalServerError para 500, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'Novo Item' }); }}> {/* Campos do formulário */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Criando...' : 'Criar Item'} </button> </form> );}
Segurança de tipos usando o cliente da API diretamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Erro de tipo se a entrada não corresponder ao schema await api.createItem(data); } catch (e) { // ✅ Tipo de erro inclui todas as possíveis respostas de erro const err = e as CreateItemError; switch (err.status) { case 400: // err.error é tipado como InvalidRequestError console.error('Erros de validação:', err.error.fieldErrors); break; case 403: // err.error é tipado como UnauthorizedError console.error('Não autorizado:', err.error.reason); break; case 500: // err.error é tipado como InternalServerError console.error('Erro no servidor:', err.error.message); break; } setError(err); } };
// UI de erro pode usar narrowing de tipos para lidar com diferentes erros if (error) { switch (error.status) { case 400: return ( <FormError message="Entrada inválida" errors={error.error.fieldErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}
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.