Pular para o conteúdo

React para FastAPI

O gerador api-connection fornece uma maneira rápida de integrar seu site React com um backend FastAPI. Ele configura toda a configuração necessária para conectar-se a 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 - 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 do seu projeto FastAPI:

    • Directoryscripts
      • generate_open_api.py Adiciona um script que gera a especificação OpenAPI para sua API
    • project.json Adiciona um novo target ao build que invoca 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 client da sua API
        • QueryClientProvider.tsx Provider do client React Query do TanStack
      • 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 client vanilla da API
        • useSigV4.tsx Adiciona um hook para assinar requisições HTTP com SigV4 (se selecionou autenticação IAM)
    • project.json Adiciona um novo target ao build que gera o client type-safe
    • .gitignore Os arquivos do client 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 FastAPI esteja disponível no site e configurada automaticamente pelo hook use<ApiName>.tsx.

    Durante o build, um client type-safe é gerado a partir da especificação OpenAPI do FastAPI. Isso adicionará três novos arquivos à aplicação React:

    • Directorysrc
      • Directorygenerated
        • Directory<ApiName>
          • types.gen.ts Tipos gerados dos modelos pydantic definidos no FastAPI
          • client.gen.ts Client type-safe para chamar sua API
          • options-proxy.gen.ts Fornece métodos para criar opções de hooks do TanStack Query

    O client type-safe gerado pode ser usado para chamar seu FastAPI a partir da aplicação React. Recomenda-se usar os hooks do TanStack Query, mas o client vanilla também está disponível.

    O gerador fornece um hook use<ApiName> para chamar a API com TanStack Query.

    Use queryOptions para obter as opções necessárias para o hook useQuery:

    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 client vanilla diretamente.

    Os hooks gerados incluem suporte a mutations usando useMutation do TanStack Query, fornecendo tratamento de estados de loading, erros e atualizações otimistas.

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    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 ? 'Creating...' : 'Create Item'}
    </button>
    {createItem.isSuccess && (
    <div className="success">
    Item created with ID: {createItem.data.id}
    </div>
    )}
    {createItem.isError && (
    <div className="error">
    Error: {createItem.error.message}
    </div>
    )}
    </form>
    );
    }

    Callbacks podem ser adicionados para diferentes estados da mutation:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    console.log('Item criado:', data);
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    console.error('Falha ao criar item:', error);
    },
    onSettled: () => {
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    Clique aqui para ver um exemplo usando o client diretamente.

    Para endpoints com parâmetro cursor, os hooks gerados suportam infinite queries usando useInfiniteQuery:

    import { useInfiniteQuery } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemList() {
    const api = useMyApi();
    const items = useInfiniteQuery({
    ...api.listItems.infiniteQueryOptions({
    limit: 10,
    }, {
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    <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
    ? 'Loading more...'
    : items.hasNextPage
    ? 'Load More'
    : 'No more items'}
    </button>
    </div>
    );
    }
    Clique aqui para ver um exemplo usando o client vanilla diretamente.

    A integração inclui tratamento de erros tipados. O tipo <operation-name>Error encapsula possíveis respostas de erro da especificação OpenAPI.

    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:
    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:
    return (
    <div>
    <h2>Não autorizado:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    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}>Create Item</button>;
    }
    Clique aqui para ver um exemplo usando o client vanilla diretamente.

    Para APIs de streaming configuradas no FastAPI, o hook useQuery atualiza automaticamente com novos chunks:

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

    Ciclo de vida do stream:

    1. Requisição HTTP é enviada

      • isLoading = true
      • fetchStatus = 'fetching'
      • data = undefined
    2. Primeiro chunk recebido

      • isLoading = false
      • fetchStatus = 'fetching'
      • data = array com primeiro chunk
    3. Chunks subsequentes

      • isLoading = false
      • fetchStatus = 'fetching'
      • data atualizado com cada chunk
    4. Stream completo

      • fetchStatus = 'idle'
      • data contém todos chunks
    Clique aqui para ver um exemplo usando o client vanilla diretamente.

    Operações com métodos HTTP PUT, POST, PATCH, DELETE são consideradas mutations por padrão. Use x-query e x-mutation para customizar.

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

    Customize o parâmetro de cursor com x-cursor:

    @app.get(
    "/items",
    openapi_extra={
    "x-cursor": "page_token"
    }
    )
    def list_items(page_token: str = None, limit: int = 10):
    # ...

    Operações são organizadas por tags OpenAPI:

    items.py
    @app.get("/items", tags=["items"])
    def list():
    # ...
    @app.post("/items", tags=["items"])
    def create(item: Item):
    # ...
    const api = useMyApi();
    const items = useQuery(api.items.list.queryOptions());
    const createItem = useMutation(api.items.create.mutationOptions());
    Clique aqui para ver um exemplo usando o client diretamente.

    Defina modelos de erro com Pydantic e handlers de exceção:

    models.py
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]
    main.py
    @app.exception_handler(NotFoundException)
    async def not_found_handler(request: Request, exc: NotFoundException):
    return JSONResponse(
    status_code=404,
    content=exc.message,
    )

    Sempre trate estados de loading e erro:

    function ItemList() {
    const api = useMyApi();
    const items = useQuery(api.listItems.queryOptions());
    if (items.isLoading) return <LoadingSpinner />;
    if (items.isError) return <ErrorMessage message={items.error.message} />;
    return (
    <ul>
    {items.data.map((item) => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    );
    }

    Implemente atualizações otimistas para melhor UX:

    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    await queryClient.cancelQueries(api.listItems.queryKey());
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    queryClient.setQueryData(api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId));
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    },
    });

    A integração fornece type safety completo. Seu IDE dará autocompletion e checagem de tipos:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    console.log(`Item criado com ID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    createItem.mutate(data); // ✅ Erro de tipo se input não corresponder
    };
    Clique aqui para ver um exemplo usando o client diretamente.