Pular para o conteúdo

React para FastAPI

O gerador 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 aos backends FastAPI de forma type-safe, 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 FastAPI funcional (gerado usando o gerador FastAPI)
  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 do seu projeto FastAPI:

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

    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 o cliente type-safe
    • .gitignore Os arquivos do cliente gerado são ignorados por padrão

    O gerador também adicionará Runtime Config à infraestrutura do seu site se não estiver presente, garantindo que a URL da API do FastAPI esteja disponível no site e configurada automaticamente pelo hook use<ApiName>.tsx.

    Durante o build, um cliente type-safe é gerado a partir da especificação OpenAPI do 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 FastAPI
          • client.gen.ts Cliente type-safe para chamar sua API
          • options-proxy.gen.ts Fornece métodos para criar opções de hooks TanStack Query para interagir com sua API

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

    Dependência do file watcher

    watch-generate:<ApiName>-client depende do comando nx watch, que requer o Nx Daemon estar em execução. Portanto, se o daemon estiver desativado, o cliente não será regenerado automaticamente ao alterar o FastAPI.

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

    Você pode usar o método queryOptions para recuperar as opções necessárias para chamar sua API usando o hook useQuery do TanStack Query:

    import { useQuery } from '@tanstack/react-query';
    import { useState, useEffect } from 'react';
    import { useMyApi } from './hooks/useMyApi';
    function MyComponent() {
    const api = useMyApi();
    const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
    if (item.isLoading) return <div>Carregando...</div>;
    if (item.isError) return <div>Erro: {item.error.message}</div>;
    return <div>Item: {item.data.name}</div>;
    }
    Clique aqui para um exemplo usando o cliente vanilla diretamente.

    Os hooks gerados incluem suporte para mutations usando o hook useMutation do TanStack Query. Isso fornece uma maneira limpa de lidar com operações de criação, atualização e exclusão com estados de carregamento, tratamento de erros e atualizações otimistas.

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    // Cria uma mutation usando as opções de mutation 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 é concluída (sucesso ou erro)
    // Bom lugar para invalidar consultas afetadas
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Clique aqui para um exemplo usando o cliente diretamente.

    Para endpoints que aceitam um parâmetro cursor como entrada, os hooks gerados fornecem suporte 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
    }, {
    // 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>
    {/* Aplana 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 um exemplo usando o cliente diretamente.

    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: '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>
    <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 no 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 um exemplo usando o cliente vanilla diretamente.

    Se você configurou seu FastAPI para stream de respostas, seu hook useQuery atualizará automaticamente seus 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 chunk subsequente 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 um exemplo usando o cliente vanilla diretamente.

    Por padrão, operações no FastAPI que usam 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.

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

    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
    }

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

    @app.get(
    "/items",
    openapi_extra={
    # Desativa 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
    }

    Os hooks e métodos do cliente gerados são organizados automaticamente com base nas tags OpenAPI dos endpoints do 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 em api.items
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    // Operações de users são agrupadas em api.users
    const users = useQuery(api.users.list.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>
    );
    }

    Esse agrupamento facilita a organização das chamadas de API e fornece melhor autocompletar na sua IDE.

    Clique aqui para um exemplo usando o cliente diretamente.

    Você pode personalizar respostas de erro no FastAPI definindo classes de exceção customizadas, 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.

    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]

    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

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

    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)

    O cliente gerado lidará automaticamente com esses tipos de erro, 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 FastAPI
    switch (error.status) {
    case 404:
    // error.error é uma string conforme especificado
    console.error('Não encontrado:', error.error);
    break;
    case 500:
    // error.error é tipado como ErrorDetails
    console.error('Erro no 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
    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 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}
    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 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();
    // 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 quaisquer refetches pendentes
    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 rollback
    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 um exemplo usando o cliente vanilla diretamente.

    A integração fornece type safety completo 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 type-safe 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 type narrowing para lidar com diferentes tipos de erro
    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: 'Novo Item' });
    }}>
    {/* Campos do formulário */}
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? 'Criando...' : 'Criar Item'}
    </button>
    </form>
    );
    }
    Clique aqui para um exemplo usando o cliente vanilla diretamente.

    Os tipos são gerados automaticamente a partir do schema OpenAPI do FastAPI, garantindo que quaisquer alterações na API sejam refletidas no código frontend após um build.

    If your FastAPI 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.