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.
Pré-requisitos
Seção intitulada “Pré-requisitos”Antes de usar este gerador, certifique-se que sua aplicação React possui:
- Um arquivo
main.tsx
que renderiza sua aplicação - Um backend FastAPI funcional (gerado usando o gerador FastAPI)
- 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>,);
Executar o Gerador
Seção intitulada “Executar o Gerador”- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - api-connection
- Preencha os parâmetros obrigatórios
- Clique em
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
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 |
Saída do Gerador
Seção intitulada “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 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
.
Geração de Código
Seção intitulada “Geração de Código”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
Usando o Código Gerado
Seção intitulada “Usando o Código Gerado”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.
Usando o Hook da API
Seção intitulada “Usando o Hook da API”O gerador fornece um hook use<ApiName>
para chamar a API com TanStack Query.
Consultas
Seção intitulada “Consultas”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>;}
Usando o client da API diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}
Mutations
Seção intitulada “Mutations”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() }); }});
Mutations usando o client da API diretamente
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); } catch (err) { setError(err); console.error('Falha ao criar item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}
Paginação com Infinite Queries
Seção intitulada “Paginação com Infinite Queries”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> );}
Paginação usando o client diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}
Tratamento de Erros
Seção intitulada “Tratamento de Erros”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>;}
Tratamento de erros com o client diretamente
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: return ( <div> <h2>Entrada inválida:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>Não autorizado:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Erro no servidor:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}
Consumindo Streams
Seção intitulada “Consumindo Streams”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:
-
Requisição HTTP é enviada
isLoading
=true
fetchStatus
='fetching'
data
=undefined
-
Primeiro chunk recebido
isLoading
=false
fetchStatus
='fetching'
data
= array com primeiro chunk
-
Chunks subsequentes
isLoading
=false
fetchStatus
='fetching'
data
atualizado com cada chunk
-
Stream completo
fetchStatus
='idle'
data
contém todos chunks
Streaming com client diretamente
function MyStreamingComponent() { const api = useMyApiClient(); const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
Personalizando o Código Gerado
Seção intitulada “Personalizando o Código Gerado”Consultas e Mutations
Seção intitulada “Consultas e Mutations”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.
x-query
Seção intitulada “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
x-mutation
Seção intitulada “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
Cursor de Paginação Customizado
Seção intitulada “Cursor de Paginação Customizado”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): # ...
Agrupamento de Operações
Seção intitulada “Agrupamento de Operações”Operações são organizadas por tags OpenAPI:
@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());
Agrupamento com client diretamente
const api = useMyApiClient();const itemsData = await api.items.list();const newItem = await api.items.create({ name: 'New Item' });
Erros Customizados
Seção intitulada “Erros Customizados”Defina modelos de erro com Pydantic e handlers de exceção:
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
@app.exception_handler(NotFoundException)async def not_found_handler(request: Request, exc: NotFoundException): return JSONResponse( status_code=404, content=exc.message, )
Melhores Práticas
Seção intitulada “Melhores Práticas”Estados de Loading
Seção intitulada “Estados de Loading”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> );}
Atualizações Otimistas
Seção intitulada “Atualizações Otimistas”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); },});
Type Safety
Seção intitulada “Type Safety”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};
Type safety com client diretamente
const handleSubmit = async (data: CreateItemInput) => { try { await api.createItem(data); // ✅ Erro de tipo se input inválido } catch (e) { const err = e as CreateItemError; // ✅ Tipos de erro específicos }};