Endpoints — Guia Conceitual

Esta página explica de forma didática o que cada endpoint do controller base entrega, quando usar, quais parâmetros recebe e o que retorna. Também aponta boas práticas, limites e exemplos copy/paste.

Visão Geral

  • Controllers: estenda AbstractCrudController (CRUD completo) ou AbstractReadOnlyController (somente leitura).
  • Anotações: use @ApiResource para declarar o path e @ApiGroup para organização do OpenAPI.
  • Service: para integrações JPA com relacionamentos LAZY, herde de AbstractBaseCrudService. Implementar apenas BaseCrudService deve ficar restrito a stubs, testes ou serviços sem dependência de contexto transacional.
  • HATEOAS (opt‑in): controlado por praxis.hateoas.enabled=true. Itens trazem self, create, update, delete quando aplicável; respostas agregadas incluem links úteis (all, filter, filter-cursor, schema).
  • Versão de dados: algumas respostas incluem o cabeçalho X-Data-Version (via getDatasetVersion() no service) para suportar cache/ETag na UI.

CRUD e Listagens

MétodoPathQuando usarParâmetrosRetornoNotas
GET /{id} Recuperar um registro específico id (path) RestApiResponse<D> 404 quando não encontrado
GET /all Listar todos com ordenação padrão RestApiResponse<List<EntityModel<D>>> Aplica @DefaultSortColumn quando presente
GET /by-ids Pré-carregar registros por IDs preservando ordem ids (query, repetido) List<D> Limite praxis.query.by-ids.max; vazio → lista vazia
POST /filter Listar com filtros e paginação page, size, sort, includeIds e body = FD RestApiResponse<Page<EntityModel<D>>> Tamanho ≤ praxis.pagination.max-size
POST /filter/cursor Paginação por cursor (keyset) after, before, size, sort e body = FD RestApiResponse<CursorPage<EntityModel<D>>> 501 se não implementado no service
POST /locate Localizar posição absoluta (índice) e página do ID id, size, sort e body = FD LocateResponse (position, page) 501 se o service não implementar

Problemas que resolve (listagens)

  • /filter: paginação tradicional + filtros ricos (26 operações), ordenação padrão automática quando nenhum sort é enviado.
  • /filter/cursor: paginação estável (keyset) para listas longas — evita saltos ao inserir/alterar dados.
  • /locate: localizar rapidamente a posição de um ID para paginar diretamente até ele.
  • /by-ids: recuperar um conjunto específico na ordem escolhida pelo usuário.

Options (id/label) para selects

MétodoPathQuando usarParâmetrosRetornoNotas
POST /options/filter Prover opções paginadas (id/label) a partir de filtros page, size, sort e body = FD Page<OptionDTO<ID>> Label via @OptionLabel ou heurísticas
GET /options/by-ids Mapear IDs para labels, preservando ordem ids (query, repetido) List<OptionDTO<ID>> Limite segue by-ids

Problemas que resolve (options)

  • /options/filter: payload leve (id/label) com paginação e filtros para combos/autocomplete.
  • /options/by-ids: reidratar valores salvos (ids) com labels corretos, na mesma ordem.

Option Sources (metadados de fontes remotas)

MétodoPathQuando usarParâmetrosRetornoNotas
GET /option-sources Listar fontes declaradas para campos remotos do recurso RestApiResponse<List<OptionSourceDescriptor>> Útil para hosts que descobrem contratos de select/autocomplete dinamicamente
POST /option-sources/filter Filtrar fontes declaradas por critérios do cliente body = OptionSourceFilterDTO RestApiResponse<List<OptionSourceDescriptor>> Reduz payload quando o host precisa apenas de um subconjunto das fontes

Problemas que resolve (option-sources)

  • /option-sources: expõe de forma canônica quais fontes remotas alimentam selects/autocompletes do recurso.
  • /option-sources/filter: evita parsing e descoberta manual no cliente quando só parte do mapa é necessária.

CRUD (escrita)

MétodoPathQuando usarParâmetrosRetornoNotas
POST / Criar novo registro body = D 201 Created + RestApiResponse<D> Location aponta para /{id}
PUT /{id} Atualizar registro id e body = D RestApiResponse<D> 404 quando não encontrado
DELETE /{id} Remover registro id 204 No Content 404 quando não encontrado
DELETE /batch Remover múltiplos registros por IDs body = List<ID> 204 No Content 400 quando lista vazia/nula

Problemas que resolve (escrita)

  • Padroniza envoltório de resposta (RestApiResponse) e links HATEOAS úteis.
  • Em read‑only, todos os endpoints de escrita retornam 405, evitando alterações indevidas.

Schemas (x‑ui / OpenAPI)

MétodoPathQuando usarParâmetrosRetornoNotas
GET /schemas Obter/derivar esquema do recurso atual 302 para /schemas/filtered Redireciona com path, operation, schemaType, idField, readOnly

Problemas que resolve (schemas)

  • Integra UIs dinâmicas: a UI consome schemas filtrados/rotas corretas por recurso.

Stats (agregações e séries)

MétodoPathQuando usarParâmetrosRetornoNotas
POST /stats/group-by Agregações por dimensão categórica body = GroupByRequestDTO RestApiResponse<List<GroupByResultDTO>> Ideal para dashboards por status, categoria ou segmento
POST /stats/timeseries Séries temporais por janela/intervalo body = TimeSeriesRequestDTO RestApiResponse<List<TimeSeriesPointDTO>> Combina filtros do recurso com agregação temporal
POST /stats/distribution Distribuição por faixas numéricas body = DistributionRequestDTO RestApiResponse<List<DistributionBucketDTO>> Útil para histogramas e análise de concentração

Problemas que resolve (stats)

  • /stats/group-by: evita endpoints ad hoc para contagens por categoria.
  • /stats/timeseries: entrega série temporal pronta para dashboards e tendências.
  • /stats/distribution: centraliza a lógica de buckets no backend canônico.

Parâmetros e Retornos (copiar/colar)

POST /filter

POST {base}/filter?page=0&size=20&sort=nome,asc&sort=id,desc
Content-Type: application/json

{ /* FD - DTO de filtro, campos com @Filterable */ }
  
  • Query: page:int, size:int (≤ praxis.pagination.max-size), sort:string[] (campo,asc|desc), includeIds:ID[].
  • Body: FD (campos anotados com @Filterable).
  • Retorno: RestApiResponse<Page<EntityModel<D>>> com links HATEOAS (quando habilitado).
  • Erros: 422 (size excede); 400 validações; 500 internos.

POST /filter/cursor

POST {base}/filter/cursor?size=50&after=eyJrIjoiY29kZS0xMjMifQ==
Content-Type: application/json

{ /* FD */ }
  
  • Query: after:string | before:string (cursores), size:int (≤ máx), sort:string[].
  • Retorno: RestApiResponse<CursorPage<EntityModel<D>>> com content, next, prev, size.
  • Erros: 501 quando o service não implementa; 422 quando size excede.

POST /locate

POST {base}/locate?size=20&id=123
Content-Type: application/json

{ /* FD */ }
  
  • Retorno: LocateResponse com position (índice zero‑based) e page (derivada de size).
  • Erros: 501 se não implementado; 422 quando size excede.

GET /all

GET {base}/all
  
  • Retorno: RestApiResponse<List<EntityModel<D>>> com links (filter, filter-cursor, schema).
  • Ordenação: aplica @DefaultSortColumn quando não há sort.

GET /by-ids

GET {base}/by-ids?ids=10&ids=7&ids=42
  
  • Retorno: List<D> na mesma ordem dos ids.
  • Limites: até praxis.query.by-ids.max itens; vazio/nulo → lista vazia.

POST /options/filter

POST {base}/options/filter?page=0&size=20
Content-Type: application/json

{ /* FD */ }
  
  • Retorno: Page<OptionDTO<ID>>. Label via @OptionLabel ou heurísticas do BaseCrudService.

GET /options/by-ids

GET {base}/options/by-ids?ids=10&ids=7
  
  • Retorno: List<OptionDTO<ID>> na ordem solicitada. Limite segue by-ids.

GET /option-sources

GET {base}/option-sources
  
  • Retorno: descritores das fontes remotas de opções do recurso atual.
  • Uso típico: bootstrap de hosts metadata-driven que montam selects/autocomplete sem mapeamento hardcoded.

POST /option-sources/filter

POST {base}/option-sources/filter
Content-Type: application/json

{ /* filtros para subconjunto de fontes */ }
  
  • Retorno: subconjunto filtrado das fontes declaradas.

POST /stats/group-by

POST {base}/stats/group-by
Content-Type: application/json

{ "field": "status", "filters": { /* FD */ } }
  
  • Retorno: agregação por chave/categoria com contagens e métricas derivadas.

POST /stats/timeseries

POST {base}/stats/timeseries
Content-Type: application/json

{ "field": "createdAt", "interval": "DAY", "filters": { /* FD */ } }
  
  • Retorno: pontos ordenados por tempo para dashboards e tendências.

POST /stats/distribution

POST {base}/stats/distribution
Content-Type: application/json

{ "field": "valorTotal", "buckets": 10, "filters": { /* FD */ } }
  
  • Retorno: buckets de distribuição com contagens por faixa.

GET /{id}

GET {base}/{id}
  
  • Retorno: RestApiResponse<D> com links (self, all, filter, filter-cursor, update/delete quando permitido).
  • Erros: 404 quando não encontrado.

POST /

curl -s -X POST "{base}" \
  -H "Content-Type: application/json" \
  -d '{ /* D */ }'
  
  • Retorno: 201 Created + Location do recurso + RestApiResponse<D>.
  • Erros: 400/422 validações; 405 em recursos read‑only.

PUT /{id}

curl -s -X PUT "{base}/{id}" \
  -H "Content-Type: application/json" \
  -d '{ /* D */ }'
  
  • Retorno: RestApiResponse<D> com links; 404 quando não encontrado; 405 em read‑only.

DELETE /{id}

curl -s -X DELETE "{base}/{id}"
  
  • Retorno: 204 No Content; 404 quando não encontrado; 405 em read‑only.

DELETE /batch

curl -s -X DELETE "{base}/batch" \
  -H "Content-Type: application/json" \
  -d '[1,2,3]'
  
  • Retorno: 204 No Content; 400 para lista vazia/nula; 405 em read‑only.

GET /schemas

GET {base}/schemas  // 302 → /schemas/filtered?path=...&operation=...&schemaType=...
  
  • Uso: UIs dinâmicas (x‑ui): expõe idField e readOnly para enriquecer componentes de UI.

Como funciona internamente

  • Pageable/Sort: PageableBuilder/SortBuilder convertem page/size/sort e aplicam fallback de getDefaultSort() do service.
  • Filtros: GenericSpecificationsBuilder converte FD + @Filterable em Specification JPA (26 operações, com relações via relation="a.b.campo").
  • CursorPage: quando implementado no service, retorna content, next, prev, size; senão 501.
  • Locate: service.locate(FD, Sort, ID) retorna OptionalLong; vazio → 501.
  • Options: BaseCrudService#getOptionMapper() projeta OptionDTO<ID>; label via @OptionLabel ou heurísticas (getLabel(), getNome()...).
  • HATEOAS: itens recebem self, create, update, delete (omitidos em read‑only). Respostas agregadas incluem links úteis (all, filter, filter-cursor, schema).
  • Headers: X-Data-Version é adicionado nas respostas que usam withVersion(...).

Erros comuns e mitigação

  • 422 Unprocessable Entity: size > praxis.pagination.max-size; excesso de IDs em by-ids/options/by-ids.
  • 404 Not Found: GET /{id} quando não existe.
  • 405 Method Not Allowed: escrita em recursos read‑only.
  • 501 Not Implemented: /filter/cursor e /locate quando o service não implementa.
  • 400 Bad Request: validações de payload/param; DELETE /batch com lista vazia.

Configurações e limites

  • praxis.pagination.max-size (padrão 200): tamanho máximo de página para /filter, /filter/cursor, /locate, /options/filter.
  • praxis.query.by-ids.max (padrão 200): número máximo de IDs aceitos em /by-ids e /options/by-ids.
  • praxis.hateoas.enabled (padrão true): habilita/omite links HATEOAS nas respostas.

Boas práticas corporativas

  • Índices: garanta índices nos campos mais filtrados/ordenados (especialmente os anotados com @DefaultSortColumn).
  • Seletividade: prefira filtros seletivos; evite NOT_IN gigantes e listas IN muito longas.
  • Datas/UTC: padronize UTC no app/DB; ON_DATE usa janela [início do dia, próximo dia).
  • Keyset: para listas muito grandes/estáveis, implemente filterByCursor no service.

Integração Frontend — Notas por Endpoint

POST /filter (grids com paginação)

  • Debounce: aplique 300–500 ms em buscas “type‑ahead”.
  • Sort múltiplo: envie repetido (?sort=nome,asc&sort=id,desc).
  • Itens “fixados”: use includeIds para priorizar selecionados na 1ª página.
  • Sincronizar URL: reflita filtros/sort/página na rota para reabrir o estado.
  • Cache: compare X-Data-Version para invalidar dados localmente.

POST /filter/cursor (infinite scroll)

  • Cursores: persista next/prev e reenvie ao rolar.
  • Sort estável: garanta tie‑break (ex.: updatedAt desc, id desc).
  • Reset: trocou filtros/sort? Descarte cursores e recomece do início.
  • Pré‑busca: opcional, pré‑carregue a próxima página ao 80% de scroll.

POST /locate (jump‑to‑row)

  • Navegação direta: calcule page = position/size e vá à página.
  • Consistência: use os mesmos filtros/sort ativos na grid.
  • UX: destaque a linha localizada ao carregar a página.
  • Fallback: 501 → desabilite “pular para linha” na UI.

Options (/options/filter e /options/by-ids)

  • Type‑ahead: debounce 250–400 ms; mínimo de 2–3 caracteres.
  • Reidratação: chame /options/by-ids ao abrir a tela para preencher labels.
  • Dependentes: use interpolação (${campo}) no endpoint de @UISchema.
  • Leve: prefira /options/filter (OptionDTO) para combos simples.

GET /by-ids (pré‑carregamento ordenado)

  • Ordem preservada: ideal para listas personalizadas/drag‑and‑drop.
  • Chunk: se exceder praxis.query.by-ids.max, divida requisições.

CRUD (GET/POST/PUT/DELETE)

  • Toasts: 201 (criado), 200 (atualizado), 204 (removido) → mensagens claras.
  • Read‑only: 405 → desabilite botões de escrita na UI.
  • Validações: exiba mensagens de RestApiResponse.errors.
  • Refresh: após POST/PUT/DELETE, recarregue grid usando X-Data-Version para decidir se invalida cache.

GET /schemas (UIs dinâmicas)

  • x‑ui: consuma controlType, validações e dicas para montar formulários/grids.
  • idField/readOnly: use para configurar chaves e bloquear edição quando necessário.

GET /all

  • Uso pontual: apenas para combos pequenos/estáveis; prefira /filter com paginação.

Anti‑patterns frequentes

  • Usar /all em listas grandes: bloqueia a UI. Prefira /filter com size controlado.
  • Keyset sem tie‑break: ordenar só por updatedAt causa gaps/duplicações. Adicione id como desempate.
  • Persistir cursores após mudar filtros/sort: cursores ficam inválidos. Sempre reset ao trocar filtros/sort.
  • Ignorar X‑Data‑Version: pode mostrar dados obsoletos. Compare o cabeçalho para invalidar cache local.
  • Enviar listas enormes em NOT_IN: caro no banco e na rede. Reavalie o critério (flags/filtros positivos).
  • Options com payload pesado: se só precisa de id/label, use /options/filter (OptionDTO), não /filter do recurso.
  • Não reenviar includeIds nas páginas seguintes: permite duplicação dos itens “fixados”. Reenvie includeIds sem reinjeção.
  • Estourar limites: respeite praxis.pagination.max-size e praxis.query.by-ids.max; se necessário, faça chunk.

Exemplo client — cursor pagination (JS)

// Estado do feed
let state = { after: null, before: null, size: 50, sort: ["updatedAt,desc","id,desc"], items: [] };

async function fetchCursorPage(direction) {
  const params = new URLSearchParams();
  params.set('size', state.size);
  state.sort.forEach(s => params.append('sort', s));
  if (direction === 'next' && state.after) params.set('after', state.after);
  if (direction === 'prev' && state.before) params.set('before', state.before);

  const res = await fetch(`/api/recurso/filter/cursor?${params.toString()}`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ /* FD */ })
  });
  if (!res.ok) throw new Error('Erro ao paginar');
  const { data } = await res.json(); // RestApiResponse

  // Atualiza cursores e conteúdo
  state.after = data.next;   // cursor para avançar
  state.before = data.prev;  // cursor para retroceder

  if (direction === 'next') state.items.push(...data.content);
  else if (direction === 'prev') state.items.unshift(...data.content);
  else state.items = data.content;
}

// Uso:
// 1) inicial
await fetchCursorPage();
// 2) ao rolar para o fim
await fetchCursorPage('next');
// 3) ao rolar para o topo
await fetchCursorPage('prev');

// Reset quando filtros/sort mudarem
function onFiltersChanged(newFilter, newSort) {
  state.after = null; state.before = null; state.items = [];
  state.sort = newSort; // sempre inclua tie-break de id
  fetchCursorPage();
}
  

Exemplo client — jump‑to‑row (/locate)

// Assumindo grid com paginação por página
const pageSize = 50;

async function jumpToRowById(id, currentFilter, currentSort) {
  const params = new URLSearchParams();
  params.set('size', String(pageSize));
  params.set('id', String(id));
  currentSort.forEach(s => params.append('sort', s)); // e.g. ["nome,asc","id,asc"]

  const res = await fetch(`/api/recurso/locate?${params.toString()}`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(currentFilter)
  });
  if (!res.ok) throw new Error('Não foi possível localizar');
  const locate = await res.json(); // { position, page }

  // 1) Ir para a página indicada
  grid.goToPage(locate.page);

  // 2) Após render, rolar/realçar o item
  requestAnimationFrame(() => {
    const row = document.querySelector(`[data-row-id="${id}"]`);
    if (row) {
      row.scrollIntoView({ block: 'center' });
      row.classList.add('flash');
      setTimeout(() => row.classList.remove('flash'), 1200);
    }
  });
}
  

Exemplos — reidratação de Options (by-ids)

// React: carregar labels iniciais a partir de IDs
import { useEffect, useState } from 'react';

function MySelect({ initialIds = [] }) {
  const [options, setOptions] = useState([]); // OptionDTO[] { id, label }

  useEffect(() => {
    if (!initialIds.length) return;
    const params = new URLSearchParams();
    initialIds.forEach(id => params.append('ids', String(id)));
    fetch(`/api/recurso/options/by-ids?${params.toString()}`)
      .then(r => r.json())
      .then(list => setOptions(list));
  }, [initialIds]);

  // Mapeie OptionDTO → shape do componente (value/label)
  const uiOptions = options.map(o => ({ value: o.id, label: o.label }));
  return