Pular para o conteúdo

React para FastAPI

O gerador api-connection fornece uma maneira rápida de integrar seu site React com seu backend FastAPI. Ele configura toda a configuração necessária para conectar-se a backends FastAPI de forma tipada, incluindo geração de clientes e hooks do TanStack Query, suporte a autenticação AWS IAM e tratamento adequado de erros.

Pré-requisitos

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 FastAPI funcional (gerado usando o gerador FastAPI)
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>,
);

Uso

Executar o Gerador

  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

    Opções

    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
    auth string IAM Authentication strategy (choose from IAM or None)

    Saída do Gerador

    O gerador fará alterações nos seguintes arquivos do seu projeto FastAPI:

    • Directoryscripts
      • generate_open_api.py Adiciona um script que gera uma especificação OpenAPI para sua API
    • project.json Um novo target é adicionado ao build para invocar o script de geração acima

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

    • Directorysrc
      • Directorycomponents
        • <ApiName>Provider.tsx Provider para o cliente da sua API
        • QueryClientProvider.tsx Provider do cliente TanStack React Query
      • 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 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á Runtime Config à infraestrutura do seu site se ainda não estiver presente, garantindo que a URL da API para seu FastAPI esteja disponível no site e configurada automaticamente pelo hook use<ApiName>.tsx.

    Geração de Código

    Durante o build, um cliente tipado é gerado a partir da especificação OpenAPI do seu FastAPI. Isso adicionará três novos arquivos à sua aplicação React:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Tipos gerados dos modelos pydantic definidos no seu FastAPI
          • 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 o Código Gerado

    O cliente tipado gerado pode ser usado para chamar seu FastAPI a partir da aplicação React. Recomenda-se usar os hooks do TanStack Query, mas você pode usar o cliente vanilla se preferir.

    Usando o Hook da API

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

    Consultas (Queries)

    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>Loading...</div>;
    if (item.isError) return <div>Error: {item.error.message}</div>;
    return <div>Item: {item.data.name}</div>;
    }
    Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

    Mutations

    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();
    // Cria uma mutation usando as opções geradas
    const createItem = useMutation(api.createItem.mutationOptions());
    const handleSubmit = (e) => {
    e.preventDefault();
    createItem.mutate({ name: 'New Item', description: 'A new 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 é concluída (sucesso ou erro)
    // Bom lugar para invalidar queries que possam ser afetadas
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Clique aqui para ver um exemplo usando o cliente diretamente.

    Paginação com Infinite Queries

    Para endpoints que aceitam um parâmetro cursor como entrada, os hooks gerados fornecem suporte para 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
    }, {
    // Certifique-se de definir 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 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.

    Tratamento de Erros

    A integração inclui tratamento de erros embutido com respostas de erro tipadas. Um tipo <operation-name>Error é gerado, encapsulando as possíveis respostas de erro definidas na especificação OpenAPI. Cada erro possui uma propriedade status e error, e ao verificar o valor de status você pode tratar tipos específicos de erros.

    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 é tipado como CreateItem400Response
    return (
    <div>
    <h2>Entrada inválida:</h2>
    <p>{createItem.error.error.message}</p>
    <ul>
    {createItem.error.error.validationErrors.map((err) => (
    <li key={err.field}>{err.message}</li>
    ))}
    </ul>
    </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 do servidor:</h2>
    <p>{createItem.error.error.message}</p>
    <p>Trace ID: {createItem.error.error.traceId}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>Criar Item</button>;
    }
    Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

    Consumindo um Stream

    Se você configurou seu FastAPI para transmitir respostas, seu hook useQuery atualizará automaticamente os dados conforme novos chunks do stream chegam.

    Por exemplo:

    function MyStreamingComponent() {
    const api = useMyApi();
    const stream = useQuery(api.myStream.queryOptions());
    return (
    <ul>
    {(stream.data ?? []).map((chunk) => (
    <li>
    {chunk.timestamp.toISOString()}: {chunk.message}
    </li>
    ))}
    </ul>
    );
    }

    Você pode usar as propriedades isLoading e fetchStatus para determinar o estado atual do stream, se necessário. Um stream segue este ciclo de vida:

    1. A requisição HTTP para iniciar o streaming é enviada

      • isLoading é true
      • fetchStatus é 'fetching'
      • data é undefined
    2. O primeiro chunk do stream é recebido

      • isLoading torna-se false
      • fetchStatus permanece 'fetching'
      • data torna-se um array contendo o primeiro chunk
    3. Chunks subsequentes são recebidos

      • isLoading permanece false
      • fetchStatus permanece 'fetching'
      • data é atualizado com cada novo chunk assim que recebido
    4. O stream é concluído

      • isLoading permanece false
      • fetchStatus torna-se 'idle'
      • data é um array de todos os chunks recebidos
    Clique aqui para ver um exemplo usando o cliente vanilla diretamente.

    Personalizando o Código Gerado

    Consultas e Mutations

    Por padrão, operações no seu FastAPI que usam os métodos HTTP PUT, POST, PATCH e DELETE são consideradas mutations, e as demais são consultas.

    Você pode alterar este comportamento usando x-query e x-mutation.

    x-query

    @app.post(
    "/items",
    openapi_extra={
    "x-query": True
    }
    )
    def list_items():
    # ...

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

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

    x-mutation

    @app.get(
    "/start-processing",
    openapi_extra={
    "x-mutation": True
    }
    )
    def start_processing():
    # ...

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

    // O hook gerado incluirá as opções personalizadas
    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    Cursor de Paginação Personalizado

    Por padrão, os hooks gerados assumem paginação por cursor com um parâmetro chamado cursor. Você pode personalizar isso usando a extensão x-cursor:

    @app.get(
    "/items",
    openapi_extra={
    # Especifica um nome diferente para o parâmetro do cursor
    "x-cursor": "page_token"
    }
    )
    def list_items(page_token: str = None, limit: int = 10):
    # ...
    return {
    "items": items,
    "page_token": next_page_token # A resposta deve incluir o cursor com o mesmo nome
    }

    Se não quiser gerar infiniteQueryOptions para uma operação, defina x-cursor como False:

    @app.get(
    "/items",
    openapi_extra={
    # Desabilita paginação por cursor para este endpoint
    "x-cursor": False
    }
    )
    def list_items(page: int = 1, limit: int = 10):
    # ...
    return {
    "items": items,
    "total": total_count,
    "page": page,
    "pages": total_pages
    }

    Agrupando Operações

    Os hooks e métodos do cliente gerados são organizados automaticamente com base nas tags OpenAPI dos endpoints do seu FastAPI. Isso ajuda a manter suas chamadas de API organizadas e facilita encontrar operações relacionadas.

    Por exemplo:

    items.py
    @app.get(
    "/items",
    tags=["items"],
    )
    def list():
    # ...
    @app.post(
    "/items",
    tags=["items"],
    )
    def create(item: Item):
    # ...
    users.py
    @app.get(
    "/users",
    tags=["users"],
    )
    def list():
    # ...

    Os hooks gerados serão agrupados por essas 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 sob api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Operações de Users são agrupadas sob api.users
    const users = useQuery(api.users.list.queryOptions());
    // Exemplo de uso
    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}>Adicionar 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 autocompletar na sua IDE.

    Clique aqui para ver um exemplo usando o cliente diretamente.

    Erros

    Você pode personalizar respostas de erro no seu FastAPI definindo classes de exceção personalizadas, handlers de exceção e especificando modelos de resposta para diferentes códigos de status. O cliente gerado lidará automaticamente com esses tipos de erro personalizados.

    Definindo Modelos de Erro Personalizados

    Primeiro, defina seus modelos de erro usando Pydantic:

    models.py
    from pydantic import BaseModel
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    Criando Exceções Personalizadas

    Depois crie classes de exceção para diferentes cenários de erro:

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    Adicionando Handlers de Exceção

    Registre handlers de exceção para converter suas exceções em respostas HTTP:

    main.py
    from fastapi import Request
    from fastapi.responses import JSONResponse
    @app.exception_handler(NotFoundException)
    async def not_found_handler(request: Request, exc: NotFoundException):
    return JSONResponse(
    status_code=404,
    content=exc.message,
    )
    @app.exception_handler(ValidationException)
    async def validation_error_handler(request: Request, exc: ValidationException):
    return JSONResponse(
    status_code=400,
    content=exc.details.model_dump(),
    )

    Especificando Modelos de Resposta

    Finalmente, especifique os modelos de resposta para diferentes códigos de status nas definições dos endpoints:

    main.py
    @app.get(
    "/items/{item_id}",
    responses={
    404: {"model": str}
    500: {"model": ErrorDetails}
    }
    )
    def get_item(item_id: str) -> Item:
    item = find_item(item_id)
    if not item:
    raise NotFoundException(message=f"Item com ID {item_id} não encontrado")
    return item
    @app.post(
    "/items",
    responses={
    400: {"model": ValidationError},
    403: {"model": str}
    }
    )
    def create_item(item: Item) -> Item:
    if not is_valid(item):
    raise ValidationException(
    ValidationError(
    message="Dados do item inválidos",
    field_errors=["nome é obrigatório"]
    )
    )
    return save_item(item)

    Usando Tipos de Erro Personalizados no 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();
    // Consulta com tratamento de erros tipado
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // Error é tipado com base nas respostas do seu FastAPI
    switch (error.status) {
    case 404:
    // error.error é uma string conforme especificado nas respostas
    console.error('Não encontrado:', error.error);
    break;
    case 500:
    // error.error é tipado como ErrorDetails
    console.error('Erro do servidor:', error.error.message);
    break;
    }
    }
    });
    // Mutation com tratamento de erros tipado
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error é tipado como ValidationError
    console.error('Erro de validação:', error.error.message);
    console.error('Erros de campo:', error.error.field_errors);
    break;
    case 403:
    // error.error é uma string conforme especificado nas respostas
    console.error('Proibido:', error.error);
    break;
    }
    }
    });
    // Renderização do componente com tratamento de erros
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error} />;
    } else {
    return <ErrorMessage message={getItem.error.error.message} />;
    }
    }
    return (
    <div>
    {/* Conteúdo do componente */}
    </div>
    );
    }
    Clique aqui para ver um exemplo usando o cliente diretamente.

    Melhores Práticas

    Lidando 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}
    details={`Trace ID: ${err.error.traceId}`}
    />
    );
    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.

    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();
    // Consulta 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 em andamento
    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 do onMutate 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.

    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 CreateItem400Response
    return (
    <FormError
    message="Entrada inválida"
    errors={error.error.validationErrors}
    />
    );
    case 403:
    // error.error é tipado como CreateItem403Response
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error é tipado como CreateItem5XXResponse para 500, 502, etc.
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: 'New 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 do seu FastAPI, garantindo que quaisquer alterações na sua API sejam refletidas no código frontend após um build.