Data Provider (CRUD)
Camada de CRUD no estilo Refine fiada nas convenções de paginação e endpoints do tempest-fastapi-sdk. Em vez de escrever uma queryFn à mão para cada lista, cada getOne, cada mutation de criar/editar/excluir, você descreve o recurso ("posts", "users") e o SDK monta a URL, serializa paginação/ordenação/filtros e invalida o cache certo no sucesso.
Por que um data provider em cima do createApiClient + TanStack Query?
O createApiClient entrega fetch tipado e a Query entrega cache. O que falta é a cola repetitiva: traduzir { page, sort, filters } para ?page=&size=&order_by=&ascending=, escolher PATCH vs PUT, e lembrar de invalidar a lista depois de criar. O data provider centraliza esse padrão uma vez, alinhado às convenções do backend FastAPI. Você troca dezenas de useQuery/useMutation quase idênticos por useList("posts"), useCreate("posts"), etc.
Quando usar
- Telas de CRUD: listagens paginadas, detalhe, formulários de criar/editar, botão de excluir.
- Backends que seguem as convenções do
tempest-fastapi-sdk(paginação offsetBasePaginationSchema, endpoints/{resource}e/{resource}/{id}). - Quando você quer cache + invalidação automática sem escrever
queryKey/invalidateQueriesà mão em cada tela.
Para chamadas que não são CRUD de recurso (RPC, agregações, endpoints custom), continue usando o createApiClient direto.
createDataProvider
Crie o provider uma vez, a partir do seu apiClient, e passe-o para a árvore via <TempestDataProvider>:
import { createApiClient, createDataProvider } from "tempest-react-sdk";
const apiClient = createApiClient({
baseURL: import.meta.env.VITE_API_URL,
});
export const dataProvider = createDataProvider(apiClient);
createDataProvider(client, options?) recebe o cliente HTTP e um objeto opcional de overrides. Os defaults já casam com o tempest-fastapi-sdk:
| Opção | Default | Para que serve |
|---|---|---|
pageParam |
"page" |
Nome do query-param do número da página. |
sizeParam |
"size" |
Nome do query-param do tamanho da página. |
sortFieldParam |
"order_by" |
Nome do query-param do campo de ordenação. |
sortOrderParam |
"ascending" |
Nome do query-param da direção da ordenação. |
sortOrderAsBoolean |
true |
true → emite booleano (order:"asc" vira true). false → emite "asc"/"desc". |
updateMethod |
"patch" |
Método HTTP usado por update ("patch" ou "put"). |
buildPath |
ver abaixo | Monta o path do recurso. Default: id == null ? "/{resource}" : "/{resource}/{id}". |
Os defaults seguem o tempest-fastapi-sdk
BasePaginationFilterSchema usa page/size/order_by/ascending, e ascending é um booleano. Por isso sortOrderAsBoolean é true por default. Se o seu backend espera ?sort=desc em vez de ?ascending=false, ajuste sortOrderParam e sortOrderAsBoolean.
Exemplo com overrides — um backend que pagina com pageSize e ordena com sort=field:desc num único param:
import { createApiClient, createDataProvider } from "tempest-react-sdk";
const apiClient = createApiClient({ baseURL: import.meta.env.VITE_API_URL });
export const dataProvider = createDataProvider(apiClient, {
sizeParam: "pageSize",
sortOrderAsBoolean: false, // emite "asc"/"desc" literais
updateMethod: "put", // PUT em vez de PATCH
buildPath: (resource, id) => (id == null ? `/api/${resource}` : `/api/${resource}/${id}`),
});
Como cada chamada vira uma requisição
| Método do provider | Requisição HTTP |
|---|---|
getList("posts", params) |
GET /posts?page=&size=&order_by=&ascending=&...filters |
getOne("posts", id) |
GET /posts/{id} |
getMany("posts", ids) |
getOne em paralelo para cada id |
create("posts", data) |
POST /posts |
update("posts", id, data) |
PATCH /posts/{id} (ou PUT com updateMethod: "put") |
deleteOne("posts", id) |
DELETE /posts/{id} |
Provider na árvore
Envolva o app com <TempestDataProvider> passando o provider criado. Ele precisa estar abaixo de um <QueryProvider> — os hooks de recurso usam o QueryClient por baixo:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryProvider, TempestDataProvider } from "tempest-react-sdk";
import "tempest-react-sdk/styles.css";
import { App } from "./App";
import { dataProvider } from "./data-provider";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryProvider>
<TempestDataProvider provider={dataProvider}>
<App />
</TempestDataProvider>
</QueryProvider>
</StrictMode>,
);
<QueryProvider> por cima, sempre
useList/useOne/useCreate/… chamam useQuery/useMutation internamente. Sem um <QueryProvider> (ou outro QueryClientProvider) acima na árvore, eles lançam o erro do TanStack Query "No QueryClient set". E sem um <TempestDataProvider> acima, os hooks lançam useDataProvider must be used within a <TempestDataProvider>.
Os hooks de recurso
Todos resolvem o provider via contexto — você só informa o nome do recurso e, opcionalmente, os parâmetros.
useList<T>(resource, params?, options?)→UseQueryResult<OffsetPage<T>, Error>useOne<T>(resource, id, options?)→UseQueryResult<T, Error>(desabilitado enquantoidfornull/undefined)useCreate<T>(resource, options?)→ mutation;mutate(data)useUpdate<T>(resource, options?)→ mutation;mutate({ id, data })useDelete<T>(resource, options?)→ mutation;mutate(id)
As mutations já invalidam o cache certo no sucesso:
| Hook | Invalida no onSuccess |
|---|---|
useCreate |
a lista do recurso |
useUpdate |
a lista e o registro useOne daquele id |
useDelete |
a lista do recurso |
Você ainda passa opções do TanStack Query
Cada hook aceita um último argumento options que é repassado para o useQuery/useMutation por baixo (menos queryKey/queryFn/mutationFn, que o SDK controla). Então staleTime, placeholderData, onError, onSuccess (chamado depois da invalidação do SDK) continuam funcionando normalmente.
As chaves de cache, quando você precisa delas
Os hooks montam as próprias chaves, mas invalidar de fora — depois de um
WebSocket avisar que um registro mudou, por exemplo — exige a mesma chave que
eles usaram. listQueryKey e oneQueryKey são exatamente essas:
import { useQueryClient } from "@tanstack/react-query";
import { listQueryKey, oneQueryKey } from "tempest-react-sdk";
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: listQueryKey("posts") });
queryClient.invalidateQueries({ queryKey: oneQueryKey("posts", 42) });
listQueryKey(resource, params?) devolve ["data", <recurso>, "list", params]
e oneQueryKey(resource, id) devolve ["data", <recurso>, "one", id]. Montar
esse array à mão funciona até o dia em que o formato mudar — as funções não.
O envelope OffsetPage<T>
useList resolve com o envelope de paginação offset do tempest-fastapi-sdk — você não mapeia nada à mão:
interface OffsetPage<T> {
items: T[]; // as linhas da página atual
total: number; // total de registros que casam, em todas as páginas
page: number; // página atual (1-based)
pages: number; // total de páginas
size?: number; // tamanho da página (convenção fastapi-pagination)
page_size?: number; // tamanho da página (convenção alternativa)
}
Exemplo completo — recurso "posts"
Listagem paginada + ordenada + filtrada, criação via mutation, e a invalidação acontecendo sozinha:
import { useState } from "react";
import { useList, useCreate } from "tempest-react-sdk";
interface Post {
id: string;
title: string;
published: boolean;
}
export function PostsPage() {
const [page, setPage] = useState(1);
const [onlyPublished, setOnlyPublished] = useState(false);
// GET /posts?page=1&size=20&order_by=created_at&ascending=false&published=true
const { data, isLoading, isError } = useList<Post>("posts", {
pagination: { page, pageSize: 20 },
sort: { field: "created_at", order: "desc" },
filters: onlyPublished ? { published: true } : undefined,
});
// POST /posts — invalida a lista de "posts" no sucesso, sem código extra
const createPost = useCreate<Post>("posts");
if (isLoading) return <p>Carregando…</p>;
if (isError) return <p>Erro ao carregar posts.</p>;
const pageData = data!; // OffsetPage<Post>
return (
<div>
<label>
<input
type="checkbox"
checked={onlyPublished}
onChange={(e) => {
setOnlyPublished(e.target.checked);
setPage(1);
}}
/>
Só publicados
</label>
<button
disabled={createPost.isPending}
onClick={() => createPost.mutate({ title: "Novo post", published: false })}
>
Criar post
</button>
<ul>
{pageData.items.map((post) => (
<li key={post.id}>
{post.title} {post.published ? "✅" : "📝"}
</li>
))}
</ul>
<footer>
Página {pageData.page} de {pageData.pages} — {pageData.total} no total
<button disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
Anterior
</button>
<button disabled={page >= pageData.pages} onClick={() => setPage((p) => p + 1)}>
Próxima
</button>
</footer>
</div>
);
}
E uma tela de edição usando useOne + useUpdate + useDelete:
import { useOne, useUpdate, useDelete } from "tempest-react-sdk";
interface Post {
id: string;
title: string;
published: boolean;
}
export function EditPost({ id }: { id: string }) {
const { data: post, isLoading } = useOne<Post>("posts", id);
const updatePost = useUpdate<Post>("posts");
const deletePost = useDelete<Post>("posts", {
onSuccess: () => console.log("post removido"),
});
if (isLoading || !post) return <p>Carregando…</p>;
return (
<div>
<h2>{post.title}</h2>
<button
disabled={updatePost.isPending}
// PATCH /posts/{id} — invalida a lista E o useOne deste id
onClick={() => updatePost.mutate({ id: post.id, data: { published: !post.published } })}
>
{post.published ? "Despublicar" : "Publicar"}
</button>
<button disabled={deletePost.isPending} onClick={() => deletePost.mutate(post.id)}>
Excluir
</button>
</div>
);
}
useOne se desabilita sozinho sem id
Quando id é null/undefined, o useOne não dispara o fetch (enabled: false). Útil em telas que recebem o id de forma assíncrona (rota, seleção) — não precisa de enabled manual no caso comum. Você ainda pode passar enabled em options para combinar com outra condição.
Recap
createDataProvider(client, options?)transforma umapiClientnum provider CRUD no estilo Refine, com defaults alinhados aotempest-fastapi-sdk(page/size/order_by/ascending,ascendingbooleano, PATCH no update).- Envolva a árvore com
<TempestDataProvider provider={…}>abaixo de um<QueryProvider>. useListresolve umOffsetPage<T>(items/total/page/pages);useOnebusca um registro e se desabilita semid.useCreate/useUpdate/useDeletesão mutations que invalidam o cache certo no sucesso — lista (create/delete) ou lista + registro (update).- Backends fora da convenção? Ajuste
pageParam/sizeParam/sortFieldParam/sortOrderParam/sortOrderAsBoolean/updateMethod/buildPath.
Veja também
- HTTP — o
createApiClientque alimenta o data provider - Query — o
<QueryProvider>obrigatório por cima - Access Control — restringir ações de CRUD por papel/permissão