React para FastAPI
O gerador connection fornece uma maneira de integrar rapidamente seu site React com seu backend FastAPI. Ele configura toda a configuração necessária para conectar-se aos seus backends FastAPI de maneira type-safe, incluindo geração de cliente e hooks TanStack Query, suporte para 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 de que sua aplicação React tenha:
- Um arquivo
main.tsxque renderiza sua aplicação - Um backend FastAPI funcional (gerado usando o gerador FastAPI)
- Cognito Auth adicionado via o gerador
ts#website#authse estiver conectando uma API que usa autenticação Cognito ou IAM
Exemplo da estrutura main.tsx necessária
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>,);Execute o Gerador
Seção intitulada “Execute o Gerador”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:connection --dry-runyarn nx g @aws/nx-plugin:connection --dry-runnpx nx g @aws/nx-plugin:connection --dry-runbunx nx g @aws/nx-plugin:connection --dry-run- 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 - connection - Preencha os parâmetros obrigatórios
- Clique em
Generate
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| sourceProject Obrigatório | string | - | O projeto de origem |
| targetProject Obrigatório | string | - | O projeto de destino para conectar |
| sourceComponent | string | - | O componente de origem para conectar (nome do componente, caminho relativo à raiz do projeto de origem, ou id do gerador). Use '.' para selecionar explicitamente o projeto como origem. |
| targetComponent | string | - | O componente de destino para conectar (nome do componente, caminho relativo à raiz do projeto de destino, ou id do gerador). Use '.' para selecionar explicitamente o projeto como destino. |
| preferInstallDependencies | boolean | true | Se deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao executar múltiplos geradores em lote (uma instalação ainda é executada se necessário para que os geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final. |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador fará alterações nos seguintes arquivos em 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 que invoca o script de geração acima
O gerador fará alterações nos seguintes arquivos em sua aplicação React:
Directorysrc
Directorycomponents
- <ApiName>Provider.tsx Provider para seu cliente de 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 de API vanilla que pode chamar sua API.
- useSigV4.tsx Adiciona um hook para assinar requisições HTTP com SigV4 (se você selecionou autenticação IAM)
- project.json Um novo target é adicionado ao build que gera um cliente type-safe
- .gitignore Os arquivos de cliente gerados são ignorados por padrão
O gerador também adicionará Runtime Config à infraestrutura do seu site, se ainda não estiver presente, o que garante que a URL da API para seu FastAPI esteja disponível no site e automaticamente configurada pelo hook use<ApiName>.tsx.
Geração de Código
Seção intitulada “Geração de Código”No momento do build, um cliente type-safe é 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 a partir dos modelos pydantic definidos em seu 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 TanStack Query
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 sua aplicação React. É recomendado fazer uso do cliente através dos hooks TanStack Query, mas você pode usar o cliente vanilla se preferir.
Dependência do File Watcher
watch-generate:<ApiName>-client depende do comando nx watch, que requer que o Nx Daemon esteja em execução. Portanto, se você desabilitou o daemon, o cliente não será regenerado automaticamente ao fazer alterações em seu 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.
Queries
Seção intitulada “Queries”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>Loading...</div>; if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}Usando o cliente de 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 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(); // Create a mutation using the generated mutation options const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <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> );}Você também pode adicionar callbacks para diferentes estados da mutation:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // This will run when the mutation succeeds console.log('Item created:', data); // You can navigate to the new item navigate(`/items/${data.id}`); }, onError: (error) => { // This will run when the mutation fails console.error('Failed to create item:', error); }, onSettled: () => { // This will run when the mutation completes (success or error) // Good place to invalidate queries that might be affected 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); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <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> );}Upload de Arquivos
Seção intitulada “Upload de Arquivos”Para endpoints que aceitam upload de arquivo, o cliente gerado envia a requisição como FormData. Defina uma operação com um corpo multipart/form-data no seu FastAPI, por exemplo usando UploadFile:
from fastapi import UploadFile
@app.post("/files")async def upload_file(file: UploadFile, description: str = "") -> FileMetadata: contents = await file.read() ...Campos binários são tipados como Blob no cliente gerado, e outros campos (como description acima) mantêm seus tipos modelados. Passe um Blob ou File — por exemplo, um obtido de um <input type="file">:
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function UploadForm() { const api = useMyApi(); const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { uploadFile.mutate({ file, description: file.name }); } };
return <input type="file" onChange={handleChange} />;}O cliente constrói um corpo FormData e deixa o fetch definir o Content-Type (incluindo o boundary multipart) automaticamente.
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, // Number of items per page }, { // Make sure you define a getNextPageParam function to return // the parameter that should be passed as the 'cursor' for the // next page getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Flatten the pages array to render all items */} <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> );}Os hooks gerados lidam automaticamente com paginação baseada em cursor se sua API suportar isso. 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);
// Fetch initial data 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]);
// Function to load more items 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 ? '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 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 is typed as CreateItem400Response return ( <div> <h2>Invalid input:</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 is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</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 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 is typed as CreateItem400Response return ( <div> <h2>Invalid input:</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 is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}Consumindo um Stream
Seção intitulada “Consumindo um Stream”Se você configurou seu FastAPI para fazer stream de respostas, seu hook useQuery atualizará automaticamente seus dados conforme novos chunks do stream chegarem.
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
Streaming usando o cliente de API diretamente
Se você configurou seu FastAPI para fazer stream de respostas>, o cliente gerado incluirá métodos type-safe para iterar assincronamente sobre chunks em seu 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:
// Generated hook will include the custom optionsconst 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={ # Specify a different parameter name for the 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={ # Disable cursor-based pagination for this 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();
// Items operations are grouped under api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users const users = useQuery(api.users.list.queryOptions());
// Usage example 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}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Esse agrupamento facilita a organização de suas chamadas de API e fornece melhor conclusão de código em seu IDE.
Operações agrupadas usando o cliente de API 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);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Você pode personalizar respostas de erro em seu FastAPI definindo classes de exceção personalizadas, manipuladores de exceção e especificando modelos de resposta para diferentes códigos de status de erro. 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”Em seguida, crie classes de exceção personalizadas 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 Manipuladores de Exceção
Seção intitulada “Adicionando Manipuladores de Exceção”Registre manipuladores 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 de erro em suas definições de endpoint:
@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 with ID {item_id} not found") 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="Invalid item data", field_errors=["name is required"] ) ) 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 personalizados, permitindo que você faça verificação de tipo e lide com diferentes respostas de erro:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the responses in your FastAPI switch (error.status) { case 404: // error.error is a string as specified in the responses console.error('Not found:', error.error); break; case 500: // error.error is typed as ErrorDetails console.error('Server error:', error.error.message); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as ValidationError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error is a string as specified in the responses console.error('Forbidden:', error.error); break; } } });
// Component rendering with error handling if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}Lidando com 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);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the responses in your FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is a string as specified in the responses console.error('Not found:', err.error); break; case 500: // err.error is typed as ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error is a string as specified in the responses console.error('Forbidden:', err.error); break; } } };
// Component rendering with error handling 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> {/* Component content */} </div> );}Melhores Práticas
Seção intitulada “Melhores Práticas”Lidar com Estados de Carregamento
Seção intitulada “Lidar com Estados de Carregamento”Sempre lide com 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 is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}Lidar com estados de carregamento usando o cliente de API 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 is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
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();
// Query to fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, context) => { // If the mutation fails, use the context returned from onMutate to roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Always refetch after error or success to ensure data is in sync with server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Failed to load items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}Atualizações otimistas usando o cliente de API diretamente
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}Segurança de Tipo
Seção intitulada “Segurança de Tipo”A integração fornece segurança de tipo completa de ponta a ponta. Seu IDE fornecerá autocompletar completo e verificação de tipo para todas as suas chamadas de API:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation for creating items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error if onSuccess callback doesn't handle the correct response type onSuccess: (data) => { // data is fully typed based on your API's response schema console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error if input doesn't match schema createItem.mutate(data); };
// Error UI can use type narrowing to handle different error types if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error is typed as CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error is typed as CreateItem5XXResponse for 500, 502, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}Segurança de tipo usando o cliente de API diretamente
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error is typed as CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error is typed as CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" 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 esquema OpenAPI do seu FastAPI, garantindo que quaisquer alterações em sua API sejam refletidas em seu código frontend após um build.
Autenticação Personalizada
Seção intitulada “Autenticação Personalizada”Se seu FastAPI usa autenticação Custom (Lambda Authorizer), você precisará editar o provider de cliente gerado para adicionar os cabeçalhos de autorização que seu autorizador espera. Procure pela configuração fetch no <ApiName>Provider.tsx gerado e adicione seu token ou chave de API aos cabeçalhos da requisição.