Pular para o conteúdo

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.

Antes de usar este gerador, certifique-se de que sua aplicação React tenha:

  1. Um arquivo main.tsx que renderiza sua aplicação
  2. Um backend FastAPI funcional (gerado usando o gerador FastAPI)
  3. Cognito Auth adicionado via o gerador ts#website#auth se 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>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
ParâmetroTipoPadrãoDescrição
sourceProject Obrigatóriostring-O projeto de origem
targetProject Obrigatóriostring-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 booleantrueSe 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.

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.

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

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.

O gerador fornece um hook use<ApiName> que você pode usar para chamar sua API com TanStack Query.

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>;
}
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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() });
}
});
Clique aqui para um exemplo usando o cliente diretamente.

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.

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.

Clique aqui para um exemplo usando o cliente diretamente.

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>;
}
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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:

  1. A requisição HTTP para iniciar o streaming é enviada

    • isLoading é true
    • fetchStatus é 'fetching'
    • data é undefined
  2. O primeiro chunk do stream é recebido

    • isLoading torna-se false
    • fetchStatus permanece 'fetching'
    • data torna-se um array contendo o primeiro chunk
  3. Chunks subsequentes são recebidos

    • isLoading permanece false
    • fetchStatus permanece 'fetching'
    • data é atualizado com cada chunk subsequente assim que é recebido
  4. O stream é concluído

    • isLoading permanece false
    • fetchStatus torna-se 'idle'
    • data é um array de todos os chunks recebidos
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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.

@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());
@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 options
const startProcessing = useMutation(api.startProcessing.mutationOptions());

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
}

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:

items.py
@app.get(
"/items",
tags=["items"],
)
def list():
# ...
@app.post(
"/items",
tags=["items"],
)
def create(item: Item):
# ...
users.py
@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.

Clique aqui para um exemplo usando o cliente diretamente.

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.

Primeiro, defina seus modelos de erro usando Pydantic:

models.py
from pydantic import BaseModel
class ErrorDetails(BaseModel):
message: str
class ValidationError(BaseModel):
message: str
field_errors: list[str]

Em seguida, crie classes de exceção personalizadas para diferentes cenários de erro:

exceptions.py
class NotFoundException(Exception):
def __init__(self, message: str):
self.message = message
class ValidationException(Exception):
def __init__(self, details: ValidationError):
self.details = details

Registre manipuladores de exceção para converter suas exceções em respostas HTTP:

main.py
from fastapi import Request
from 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(),
)

Finalmente, especifique os modelos de resposta para diferentes códigos de status de erro em suas definições de endpoint:

main.py
@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)

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>
);
}
Clique aqui para um exemplo usando o cliente diretamente.

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>
);
}
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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>
);
}
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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>
);
}
Clique aqui para um exemplo usando o cliente vanilla diretamente.

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.

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.