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:
- Um arquivo
main.tsx
que renderiza sua aplicação - 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
- 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
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>;}
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>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}
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() }); }});
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: 'New Item', description: 'A new 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
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.
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
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>;}
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: 'New 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 do 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 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:
-
A requisição HTTP para iniciar o streaming é enviada
isLoading
étrue
fetchStatus
é'fetching'
data
éundefined
-
O primeiro chunk do stream é recebido
isLoading
torna-sefalse
fetchStatus
permanece'fetching'
data
torna-se um array contendo o primeiro chunk
-
Chunks subsequentes são recebidos
isLoading
permanecefalse
fetchStatus
permanece'fetching'
data
é atualizado com cada novo chunk assim que recebido
-
O stream é concluído
isLoading
permanecefalse
fetchStatus
torna-se'idle'
data
é um array de todos os chunks recebidos
Streaming usando o cliente da API diretamente
Se você configurou seu FastAPI para transmitir respostas, o cliente gerado incluirá métodos tipados 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
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 personalizadasconst 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:
@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 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.
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 sob api.items const itemsData = await api.items.list(); setItems(itemsData);
// Operações de Users são agrupadas sob 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: 'New 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>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
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:
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:
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:
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
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
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> );}
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 seu FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error é uma string conforme especificado nas respostas console.error('Não encontrado:', err.error); break; case 500: // err.error é tipado como ErrorDetails console.error('Erro do 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 nas respostas 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
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
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> );}
Atualizações otimistas usando o cliente diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Remove o item otimistamente 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> );}
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> );}
Segurança de tipos 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 do servidor:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// UI de erro pode usar narrowing de tipos para lidar com diferentes erros 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 seu FastAPI, garantindo que quaisquer alterações na sua API sejam refletidas no código frontend após um build.