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 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.
Pré-requisitos
Seção intitulada “Pré-requisitos”Antes de usar este gerador, certifique-se que sua aplicação React possui:
- Um arquivo
main.tsxque renderiza sua aplicação - Um backend FastAPI funcional (gerado usando o gerador FastAPI)
- Autenticação Cognito adicionada via gerador
ts#react-website-authse 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-connectionyarn nx g @aws/nx-plugin:api-connectionnpx nx g @aws/nx-plugin:api-connectionbunx nx g @aws/nx-plugin:api-connectionVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:api-connection --dry-runyarn nx g @aws/nx-plugin:api-connection --dry-runnpx nx g @aws/nx-plugin:api-connection --dry-runbunx 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 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.
Geração de Código
Seção intitulada “Geração de Código”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
Usando o Código Gerado
Seção intitulada “Usando o Código Gerado”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.
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.
Usando o Hook da API
Seção intitulada “Usando o Hook da API”O gerador fornece um hook use<ApiName> que você pode usar para chamar sua API com TanStack Query.
Consultas
Seção intitulada “Consultas”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>;}Usando o cliente 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>Carregando...</div>; if (error) return <div>Erro: {error.message}</div>;
return <div>Item: {item.name}</div>;}Mutations
Seção intitulada “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 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() }); }});Mutations usando o cliente 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: 'Novo Item', description: 'Um novo item' }); setCreatedItem(newItem); // Você pode navegar para o novo item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Falha ao criar item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Campos do formulário */} <button type="submit" disabled={isLoading} > {isLoading ? 'Criando...' : 'Criar Item'} </button>
{createdItem && ( <div className="success"> Item criado com ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Erro: {error.message} </div> )} </form> );}Paginação com Infinite Queries
Seção intitulada “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 }, { // 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.
Paginação usando o cliente da API diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Busca dados iniciais useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Função para carregar mais itens 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); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Carregando mais...' : nextCursor ? 'Carregar Mais' : 'Sem mais itens'} </button> </div> );}Tratamento de Erros
Seção intitulada “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: '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>;}Tratamento de erros usando o cliente da API diretamente
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'Novo Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error é tipado como CreateItem400Response 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: // error.error é tipado como CreateItem403Response return ( <div> <h2>Não autorizado:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error é tipado como CreateItem5XXResponse return ( <div> <h2>Erro no servidor:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Criar Item</button>;}Consumindo um Stream
Seção intitulada “Consumindo um Stream”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:
-
A requisição HTTP para iniciar o streaming é enviada
isLoadingétruefetchStatusé'fetching'dataéundefined
-
O primeiro chunk do stream é recebido
isLoadingtorna-sefalsefetchStatuspermanece'fetching'datatorna-se um array contendo o primeiro chunk
-
Chunks subsequentes são recebidos
isLoadingpermanecefalsefetchStatuspermanece'fetching'dataé atualizado com cada chunk subsequente assim que é recebido
-
O stream é concluído
isLoadingpermanecefalsefetchStatustorna-se'idle'dataé um array de todos os chunks recebidos
Stream usando o cliente da API diretamente
Se você configurou seu FastAPI para stream de respostas, o cliente gerado incluirá métodos type-safe para iterar assincronamente sobre chunks do stream usando sintaxe for await.
Por exemplo:
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”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.
x-query
Seção intitulada “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
Seção intitulada “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 personalizadasconst startProcessing = useMutation(api.startProcessing.mutationOptions());Cursor de Paginação Personalizado
Seção intitulada “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 }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 }Agrupando Operações
Seção intitulada “Agrupando Operações”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:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@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.
Operações agrupadas usando o cliente diretamente
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Carrega dados useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Operações de items são agrupadas em api.items const itemsData = await api.items.list(); setItems(itemsData);
// Operações de users são agrupadas em api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Erro ao buscar dados:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Cria item usando o método agrupado const newItem = await api.items.create({ name: 'Novo Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Erro ao criar item:', error); } };
if (isLoading) { return <div>Carregando...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Adicionar Item</button>
<h2>Usuários</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}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.
Definindo Modelos de Erro Personalizados
Seção intitulada “Definindo Modelos de Erro Personalizados”Primeiro, defina seus modelos de erro usando Pydantic:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]Criando Exceções Personalizadas
Seção intitulada “Criando Exceções Personalizadas”Crie classes de exceção para diferentes cenários de erro:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = detailsAdicionando Handlers de Exceção
Seção intitulada “Adicionando Handlers de Exceção”Registre handlers de exceção para converter suas exceções em respostas HTTP:
from fastapi import Requestfrom 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
Seção intitulada “Especificando Modelos de Resposta”Finalmente, especifique os modelos de resposta para diferentes códigos de status nas definições dos endpoints:
@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
Seção intitulada “Usando Tipos de Erro Personalizados no React”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> );}Tratando erros personalizados com o cliente diretamente
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Busca item com tratamento de erros useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error é tipado com base nas respostas do FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error é uma string conforme especificado console.error('Não encontrado:', err.error); break; case 500: // err.error é tipado como ErrorDetails console.error('Erro no servidor:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Cria item com tratamento de erros const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error é tipado como ValidationError console.error('Erro de validação:', err.error.message); console.error('Erros de campo:', err.error.field_errors); break; case 403: // err.error é uma string conforme especificado console.error('Proibido:', err.error); break; } } };
// Renderização do componente com tratamento de erros if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Conteúdo do componente */} </div> );}Melhores Práticas
Seção intitulada “Melhores Práticas”Lidando com Estados de Carregamento
Seção intitulada “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> );}Lidando com estados de carregamento usando o cliente diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; 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.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Atualizações Otimistas
Seção intitulada “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 quaisquer refetches pendentes 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 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> );}Atualizações otimistas usando o cliente diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Remove otimistamente o item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restaura os items anteriores em caso de erro setItems(previousItems); console.error('Falha ao excluir item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Excluir</button> </li> ))} </ul> );}Type Safety
Seção intitulada “Type Safety”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> );}Type safety usando o cliente diretamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Erro de tipo se a entrada não corresponder ao schema await api.createItem(data); } catch (e) { // ✅ Tipo de erro inclui todas as possíveis respostas de erro const err = e as CreateItemError; switch (err.status) { case 400: // err.error é tipado como CreateItem400Response console.error('Erros de validação:', err.error.validationErrors); break; case 403: // err.error é tipado como CreateItem403Response console.error('Não autorizado:', err.error.reason); break; case 500: case 502: // err.error é tipado como CreateItem5XXResponse console.error( 'Erro no servidor:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// UI de erro pode usar type narrowing para lidar com diferentes tipos de erro if (error) { switch (error.status) { case 400: return ( <FormError message="Entrada inválida" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}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.