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) ouAbstractReadOnlyController(somente leitura). - Anotações: use
@ApiResourcepara declarar o path e@ApiGrouppara organização do OpenAPI. - Service: para integrações JPA com relacionamentos
LAZY, herde deAbstractBaseCrudService. Implementar apenasBaseCrudServicedeve ficar restrito a stubs, testes ou serviços sem dependência de contexto transacional. - HATEOAS (opt‑in): controlado por
praxis.hateoas.enabled=true. Itens trazemself,create,update,deletequando 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(viagetDatasetVersion()no service) para suportar cache/ETag na UI.
CRUD e Listagens
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
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)
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)
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étodo | Path | Quando usar | Parâmetros | Retorno | Notas |
|---|---|---|---|---|---|
| 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)
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 (
sizeexcede); 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>>>comcontent,next,prev,size. - Erros: 501 quando o service não implementa; 422 quando
sizeexcede.
POST /locate
POST {base}/locate?size=20&id=123
Content-Type: application/json
{ /* FD */ }
- Retorno:
LocateResponsecomposition(índice zero‑based) epage(derivada desize). - Erros: 501 se não implementado; 422 quando
sizeexcede.
GET /all
GET {base}/all
- Retorno:
RestApiResponse<List<EntityModel<D>>>com links (filter,filter-cursor,schema). - Ordenação: aplica
@DefaultSortColumnquando não hásort.
GET /by-ids
GET {base}/by-ids?ids=10&ids=7&ids=42
- Retorno:
List<D>na mesma ordem dosids. - Limites: até
praxis.query.by-ids.maxitens; 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@OptionLabelou heurísticas doBaseCrudService.
GET /options/by-ids
GET {base}/options/by-ids?ids=10&ids=7
- Retorno:
List<OptionDTO<ID>>na ordem solicitada. Limite segueby-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/deletequando permitido). - Erros: 404 quando não encontrado.
POST /
curl -s -X POST "{base}" \
-H "Content-Type: application/json" \
-d '{ /* D */ }'
- Retorno:
201 Created+Locationdo 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
idFieldereadOnlypara enriquecer componentes de UI.
Como funciona internamente
- Pageable/Sort:
PageableBuilder/SortBuilderconvertempage/size/sorte aplicam fallback degetDefaultSort()do service. - Filtros:
GenericSpecificationsBuilderconverteFD+@Filterableem Specification JPA (26 operações, com relações viarelation="a.b.campo"). - CursorPage: quando implementado no service, retorna
content,next,prev,size; senão 501. - Locate:
service.locate(FD, Sort, ID)retornaOptionalLong; vazio → 501. - Options:
BaseCrudService#getOptionMapper()projetaOptionDTO<ID>; label via@OptionLabelou 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 usamwithVersion(...).
Erros comuns e mitigação
- 422 Unprocessable Entity:
size>praxis.pagination.max-size; excesso de IDs emby-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/cursore/locatequando o service não implementa. - 400 Bad Request: validações de payload/param;
DELETE /batchcom 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-idse/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_INgigantes e listasINmuito longas. - Datas/UTC: padronize UTC no app/DB;
ON_DATEusa janela [início do dia, próximo dia). - Keyset: para listas muito grandes/estáveis, implemente
filterByCursorno service.
Links úteis
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
includeIdspara priorizar selecionados na 1ª página. - Sincronizar URL: reflita filtros/sort/página na rota para reabrir o estado.
- Cache: compare
X-Data-Versionpara invalidar dados localmente.
POST /filter/cursor (infinite scroll)
- Cursores: persista
next/preve 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/sizee 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-idsao abrir a tela para preencher labels. - Dependentes: use interpolação (
${campo}) noendpointde@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-Versionpara 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
/filtercom paginação.
Anti‑patterns frequentes
- Usar /all em listas grandes: bloqueia a UI. Prefira
/filtercomsizecontrolado. - Keyset sem tie‑break: ordenar só por
updatedAtcausa gaps/duplicações. Adicioneidcomo 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/filterdo recurso. - Não reenviar includeIds nas páginas seguintes: permite duplicação dos itens “fixados”. Reenvie
includeIdssem reinjeção. - Estourar limites: respeite
praxis.pagination.max-sizeepraxis.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 ;
}
// Angular: serviço e uso no componente
// service.ts
getOptionsByIds(ids: (number|string)[]) {
const params = new HttpParams({ fromObject: ids.reduce((acc, id) => { (acc['ids'] ||= [] as any).push(String(id)); return acc; }, {} as any) });
return this.http.get<OptionDTO<any>[]>(`/api/recurso/options/by-ids`, { params });
}
// component.ts
ngOnInit() {
if (this.initialIds?.length) {
this.svc.getOptionsByIds(this.initialIds).subscribe(list => {
this.selectOptions = list.map(o => ({ value: o.id, label: o.label }));
this.form.patchValue({ field: this.initialIds }); // reidrata valores
});
}
}