Pular para o conteúdo

Reagir para API Smithy

O gerador 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.

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

  1. Um arquivo main.tsx que renderiza sua aplicação
  2. Um backend de API TypeScript Smithy funcional (gerado usando o gerador ts#smithy-api)
  3. 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>,
);
  1. Instale o Nx Console VSCode Plugin se ainda não o fez
  2. Abra o console Nx no VSCode
  3. Clique em Generate (UI) na seção "Common Nx Commands"
  4. Procure por @aws/nx-plugin - connection
  5. Preencha os parâmetros obrigatórios
    • Clique em Generate
    Parâmetro Tipo Padrão Descrição
    sourceProject Obrigatório string - O projeto de origem
    targetProject Obrigatório string - 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.

    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 que pode chamar sua 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.

    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 para interagir com sua API usando TanStack Query

    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.

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

    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() });
    }
    });
    Clique aqui para ver 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, // 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.

    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: '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>;
    }
    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 := {
    // Parâmetro de entrada chamado 'cursor' faria esta operação ser tratada como paginada por padrão
    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();
    // 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.

    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 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>
    );
    }
    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 é 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>
    );
    }
    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 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 otimisticamente 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>
    );
    }
    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();
    // 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>
    );
    }
    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.

    If your Smithy API uses Custom authentication (Lambda Authorizer), you will need to edit the generated client provider to add the authorization headers your authorizer expects. Look for the fetch configuration in the generated <ApiName>Provider.tsx and add your token or API key to the request headers.