Pular para o conteúdo

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.

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 - api-connection
  5. Preencha os parâmetros obrigatórios
    • Clique em Generate
    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

    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.

    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.

    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:

    Terminal window
    pnpm 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:

    Terminal window
    pnpm 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:

    Terminal window
    pnpm nx run <WebsiteProject>:"watch-generate:<ApiName>-client"

    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.

    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.

    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.

    Se não precisar personalizar o cliente gerado, você pode excluir extensions.smithy com segurança

    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")
    @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.

    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.

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