Pular para o conteúdo

API REST

O Rebase gera automaticamente uma API completa a partir das definições das suas coleções:

  • API REST — Endpoints CRUD para cada coleção em /api/data/:slug
  • Especificação OpenAPI — Especificação legível por máquina em /api/docs
  • Swagger UI — Explorador interativo de API em /api/swagger (apenas em modo de desenvolvimento)

Nenhum código é necessário — defina suas coleções e a API surge automaticamente.

Para cada coleção, os seguintes endpoints são gerados. Todas as outras rotas montadas pelo backend — auth, storage, admin, meta — estão no índice de endpoints.

Método Caminho Descrição
GET /api/data/:slug Listar entidades
POST /api/data/:slug/query A mesma listagem, com seus parâmetros de consulta em um corpo JSON — para uma consulta longa demais para uma URL. Veja Consultas longas demais para uma URL
GET /api/data/:slug/count Contar entidades
GET /api/data/:slug/aggregate count(), sum(), avg(), min(), max(), opcionalmente agrupados. Aceita os mesmos filtros que o endpoint de listagem, e o RLS se aplica às linhas que estão sendo agregadas — veja Consultas
GET /api/data/:slug/:id Obter uma única entidade
POST /api/data/:slug Criar um registro
PATCH /api/data/:slug/:id Atualizar um registro (parcial — apenas as propriedades enviadas são gravadas)
DELETE /api/data/:slug/:id Excluir um registro
POST /api/data/:slug/bulk Criar várias entidades em uma única transação
PATCH /api/data/:slug/bulk Atualizar várias entidades em uma única transação
POST /api/data/:slug/bulk/delete Excluir várias entidades em uma única transação
POST /api/data/_batch Gravar entre coleções em uma única transação

Relações aninhadas são acessíveis por meio de caminhos de URL:

GET /api/data/authors/42/posts → list author's posts
POST /api/data/authors/42/posts/query → the same listing, parameters in the body (0.24+)
POST /api/data/authors/42/posts/count → the same count, parameters in the body (0.24+)
GET /api/data/authors/42/posts/7 → get a specific post by author
POST /api/data/authors/42/posts → create a post for author
PATCH /api/data/authors/42/posts/7 → update the post
DELETE /api/data/authors/42/posts/7 → delete the post

Para lidar com profundidades arbitrárias de subcoleções aninhadas, o Rebase roteia requisições recebidas usando a regex de parâmetro :rest{.+} do Hono. O mecanismo interno de análise de segmentos analisa os caminhos contando segmentos separados por barra:

  • Contagem ímpar de segmentos (ex.: authors/42/posts -> 3 segmentos) representa uma requisição de listagem de coleção.
  • Contagem par de segmentos (ex.: authors/42/posts/7 -> 4 segmentos) representa uma operação em um ID de entidade específico. O último segmento é extraído como o entityId de destino.

O mecanismo filtra namespaces reservados do sistema (ex.: history) da análise de segmentos de caminho para evitar colisões com endpoints integrados.

Todos os endpoints de dados exigem autenticação por padrão. Inclua um token Bearer no cabeçalho Authorization:

curl -H "Authorization: Bearer <access-token>" \
https://api.example.com/api/data/products

Para chamadas de servidor para servidor, use a chave de serviço:

curl -H "Authorization: Bearer <service-key>" \
https://api.example.com/api/data/products

A filtragem de uma lista — operadores ?field=op.value, grupos or/and, o dialeto JSON where e como se combinam — tem uma página própria.

Use orderBy com o formato field:direction:

# Sort by price descending
GET /api/data/products?orderBy=price:desc
# Sort by name ascending (default)
GET /api/data/products?orderBy=name:asc

Uma direção ausente é asc. Uma direção que não seja nem asc nem desc, ou um campo que a coleção não possui, resulta em um 400 — e não em um 200 com as linhas na ordem que o banco de dados bem entender, o que seria indistinguível de uma ordenação que funcionou.

A forma abreviada suporta uma chave. Para mais chaves, passe um array JSON — a segunda chave serve de desempate entre linhas consideradas iguais pela primeira:

# By category, and newest first within each category
GET /api/data/products?orderBy=[{"field":"category"},{"field":"createdAt","direction":"desc"}]

Ambas as grafias funcionam em todas as rotas que listam linhas, incluindo as aninhadas (/api/data/authors/:id/posts). Toda ordenação termina no id da linha em ordem decrescente, quer você tenha solicitado ou não: é isso que torna a ordenação total, e paginar sobre uma ordem que não é total repete e pula linhas.

Um parâmetro ?orderBy= repetido não é uma ordenação de múltiplas chaves — o último prevalece, assim como ocorre com qualquer outro parâmetro de consulta. Use o array.

Por padrão, os NULLs são ordenados por último em ordem ascendente e primeiro em ordem descendente, que é a própria convenção do Postgres. Um terceiro segmento separado por dois-pontos define o contrário:

# Newest first, with the undated rows at the end rather than the top
GET /api/data/posts?orderBy=publishedAt:desc:last

A forma de array JSON aceita uma chave "nulls" para a mesma finalidade:

GET /api/data/posts?orderBy=[{"field":"publishedAt","direction":"desc","nulls":"last"}]

Qualquer valor diferente de first ou last resulta em um 400, e não em uma ordem silenciosamente diferente. O cursor abaixo respeita o que a ordenação declarou, de modo que a paginação sobre uma chave anulável permanece correta em qualquer posicionamento.

Use limit e offset, ou page — um ou outro:

# Limit and offset
GET /api/data/products?limit=20&offset=40
# Page-based (uses the default limit of 50)
GET /api/data/products?page=3

O limite padrão é 50, e o máximo é 1000. Ambos provêm de DEFAULT_LIST_LIMIT / MAX_LIST_LIMIT, que a especificação OpenAPI gerada também relata — um limit acima do máximo é rejeitado em vez de ser ajustado/limitado.

Todos os três parâmetros de janela são recusados em vez de reparados, e cada um identifica a si próprio: INVALID_LIMIT, INVALID_OFFSET (um número inteiro maior ou igual a 0) e INVALID_PAGE (um número inteiro maior ou igual a 1). Uma janela silenciosamente diferente da solicitada não pode ser diferenciada de ter chegado ao final da coleção, razão pela qual nenhum deles é ajustado ou ignorado.

Pelo mesmo motivo, page e offset juntos resultam em um 400 PAGE_WITH_OFFSET, em vez de um deles prevalecer silenciosamente.

meta.total é um COUNT(*) sobre todas as linhas correspondentes — numa tabela grande, a maior parte do custo de uma requisição de listagem. ?count=none (GET /api/data/orders?count=none) a dispensa: sem meta.total, enquanto meta.hasMore continua exato (a leitura pede uma linha além da página). ?count=exact é o padrão; qualquer outro valor resulta em um 400 INVALID_COUNT. Uma leitura com ?distinct=true nunca é contada, nem uma busca vetorial com vector_threshold (veja Busca Vetorial).

O offset reconta as linhas a cada requisição, portanto, uma linha inserida ou excluída entre duas páginas desloca a janela e a navegação pula ou repete linhas silenciosamente. Em vez disso, ?after= faz uma busca direta (seek): a próxima página começa estritamente após a última linha entregue.

Toda resposta de listagem traz meta.nextCursor enquanto houver outra página. Envie-o de volta inalterado:

GET /api/data/orders?orderBy=createdAt:desc&limit=100
# → meta.nextCursor = "eyJrIjpbWyJjcmVhdGVkX2F0Iiw…"
GET /api/data/orders?orderBy=createdAt:desc&limit=100&after=eyJrIjpbWyJjcmVhdGVkX2F0Iiw…

O cursor é opaco — ele codifica as chaves de ordenação e os valores da última linha para elas —, portanto três regras se aplicam, cada uma resultando em um 400 em vez de uma página incorreta:

Situação Código
after com offset ou page CURSOR_WITH_OFFSET — ambos definem onde a página começa
after com um orderBy diferente daquele sob o qual foi emitido CURSOR_ORDER_MISMATCH
Um cursor que esta API não emitiu INVALID_CURSOR

Uma requisição que não especifica nenhum orderBy adota o do cursor, portanto passar meta.nextCursor de volta sem reafirmar a ordenação funciona.

Ordenações por múltiplas chaves e chaves anuláveis paginam corretamente: a comparação é construída sobre cada chave em ordem, com o posicionamento de NULL que a ordenação declarou. A única ordenação que nenhum cursor pode descrever é a relevância (_score) — calculada por consulta e não armazenada em lugar nenhum —, e essa listagem simplesmente não traz nenhum nextCursor.

?fields= restringe a leitura às colunas nomeadas. É uma projeção aplicada diretamente à consulta, e não um corte na resposta:

GET /api/data/posts?fields=id,title&limit=50

A chave primária sempre é retornada (uma linha que não pode ser endereçada não pode ser atualizada, excluída ou paginada adiante — e o cursor é derivado dela), e as colunas com excludeFromApi permanecem ocultas, quer sejam nomeadas ou não. Uma coluna desconhecida resulta em um 400 UNKNOWN_FIELD, em vez de uma linha silenciosamente sem um campo.

Uma relação nomeada em ?include= volta independentemente de ?fields= a nomear: ?include=author&fields=title retorna title, a chave e author. Nomear a relação também em ?fields= é permitido e não muda nada.

?distinct=true agrupa linhas idênticas com base nessas colunas:

# The statuses actually in use
GET /api/data/posts?fields=status&distinct=true

Ele é recusado (400) quando utilizado junto a um searchString com classificação ou a uma busca vetorial, que anexam uma pontuação por linha que torna cada linha distinta por definição, e quando orderBy especifica uma coluna que fields não retorna (DISTINCT_ORDER_BY_NOT_SELECTED) — o Postgres não pode ordenar uma leitura DISTINCT por uma expressão fora da sua lista de seleção.

?fields= e ?distinct= também funcionam na rota de busca por ID e nas rotas de subcoleções aninhadas.

As respostas de listagem incluem metadados de paginação:

{
"data": [
{ "id": 1, "name": "Widget", "price": 29.99 },
{ "id": 2, "name": "Gadget", "price": 49.99 }
],
"meta": {
"total": 150,
"limit": 20,
"offset": 0,
"hasMore": true,
"nextCursor": "eyJrIjpbWyJpZCIsImRlc2MiXV0sInYiOnsiaWQiOjJ9LCJpIjoyfQ"
}
}

nextCursor está presente enquanto hasMore for true e a página tiver retornado pelo menos uma linha; ele fica ausente na última página e em ordenações que nenhum cursor pode descrever.

As respostas de entidade única retornam um objeto simples:

{
"id": 1,
"name": "Widget",
"price": 29.99,
"createdAt": "2026-01-15T10:30:00Z"
}

Toda falha, de qualquer rota, retorna em um único envelope:

{
"error": {
"message": "Unknown filter operator 'contains' on field 'title'.",
"code": "UNKNOWN_FILTER_OPERATOR",
"details": { "field": "title", "operator": "contains" },
"requestId": "9f1c0b8e-4d2a-4e1b-9d0f-2c7a5b3e6a11"
}
}

message e code estão sempre presentes. details aparece quando a recusa é sobre algo — o campo incorreto, os caminhos que falharam. requestId aparece quando a requisição continha um cabeçalho X-Request-ID ou quando um lhe foi atribuído; ele também é refletido no cabeçalho da resposta e é a referência a ser citada em um relatório de bug.

Faça o tratamento condicional com base no code, nunca na message ou apenas no status. Os códigos estão em SCREAMING_SNAKE_CASE e são estáveis; as mensagens são escritas para uma pessoa lendo um console e estão sujeitas a alterações. O status HTTP fica na resposta, não no corpo.

Status Código comum Significado
400 BAD_REQUEST, INVALID_INPUT, INVALID_LIMIT, INVALID_OFFSET, INVALID_PAGE A requisição está malformada ou solicita algo impossível
401 UNAUTHORIZED Nenhuma credencial informada, ou uma credencial que não identifica ninguém
403 FORBIDDEN, SCOPE_MISSING, WRITE_DENIED Uma credencial que identifica alguém sem a devida permissão
404 NOT_FOUND O recurso solicitado não existe
409 CONFLICT Conflito de estado — chave duplicada, árvore desatualizada (dirty tree)
501 varia O recurso existe, mas não está configurado nesta implantação
503 SERVICE_UNAVAILABLE Uma dependência está fora do ar; a requisição nunca chegou até ela

Uma funcionalidade ausente porque esta implantação não a habilitou responde com 501 com um código e um motivo, e não 404 — um 404 não explicado em uma rota recém-chamada pela interface pareceria uma falha no deploy.

As rotas adicionam seus próprios códigos mais específicos sobre estes (EMAIL_EXISTS, TOKEN_EXPIRED, UNKNOWN_FILTER_OPERATOR, …), portanto considere a lista de códigos como aberta. O SDK tipado converte todos eles em um único RebaseApiError contendo status, code e details — veja Tratamento de erros.

Use searchString para busca de texto completo (full-text search) em campos de texto:

GET /api/data/products?searchString=wireless%20keyboard

Uma coleção com uma propriedade vector aceita uma busca por vizinhos mais próximos na listagem: ?vector_search=embedding&vector=[0.15,0.22,-0.05], com vector_distance e vector_threshold opcionais. Os parâmetros, os limites e o comportamento dos filtros sobre o índice estão em Busca Vetorial.

POST /api/data/:slug/query (esta listagem) e POST /api/data/:slug/count recebem os parâmetros de consulta em um corpo JSON, para uma consulta cuja URL não caberia — um embedding, uma lista in longa. Veja Consultas longas demais para uma URL.

Use o parâmetro include para incorporar entidades relacionadas:

# Include specific relations
GET /api/data/articles?include=author,categories
# Include all relations, one hop deep
GET /api/data/articles?include=*
# A relation of a relation — up to three hops
GET /api/data/articles?include=comments.author

Um nome que não seja uma relação da coleção resulta em um 400 UNKNOWN_RELATION, em qualquer nível. Anteriormente isso era ignorado, o que respondia com 200 trazendo o campo simplesmente ausente — indistinguível de uma linha que genuinamente não tem nenhuma linha relacionada, de modo que um erro de digitação parecia exatamente dados vazios. Um caminho com profundidade superior a três saltos resulta em INCLUDE_TOO_DEEP.

A forma separada por vírgulas não tem onde especificar um limit por relação, portanto include também aceita JSON — diferenciado por uma chave de abertura no início:

GET /api/data/posts?include={"comments":{"limit":5,"where":{"published":["==",true]},"orderBy":"createdAt:desc","fields":["id","body"],"include":{"author":true}}}
Chave Significado
limit Linhas por linha pai, não em toda a página. Aplicado pela própria consulta, portanto as linhas além dele nunca são lidas do banco de dados
where O mesmo dialeto de filtro que o where de nível superior usa
logical Um grupo or/and/not sobre as linhas relacionadas
orderBy A mesma grafia de ordenação, incluindo o posicionamento de NULL. Os empates, e um limit sem orderBy, seguem a chave da linha relacionada, em ordem ascendente
fields Colunas da linha relacionada; sua chave primária sempre permanece
include Relações da linha relacionada, por sua vez
count true também devolve o total de cada pai em _count ("_count":{"comments":37}), contado pela mesma instrução. Apenas relações para-muitos. Veja Contando o que um limit deixa de fora

true significa “carregar por completo”, portanto {"author":true} e author são a mesma requisição. Ambas as grafias funcionam na rota de listagem, na rota de busca por ID e nas rotas de subcoleções aninhadas.

Cada salto é uma consulta em lote para toda a página, nunca uma por linha.

As relações incluídas são incorporadas diretamente na resposta:

{
"id": 1,
"title": "Getting Started",
"authorId": 42,
"author": {
"id": 42,
"name": "Jane Doe",
"email": "jane@example.com",
"companyId": 7
}
}

Uma linha incluída é o que a rota do próprio alvo (GET /api/data/authors/42) serve: as mesmas colunas e codificações, chaves estrangeiras como colunas, sem objetos de referência.

Chaves de idempotência, escritas condicionais (ETag / If-Match), operações de campo ($inc, $push, $pull, $merge), upsert em chave natural, Prefer: return=minimal e o endpoint entre coleções POST /api/data/_batch estão todos em sua própria página: Escrita via REST.

No Postgres, toda mutação REST (POST, PATCH, DELETE) roda os callbacks da sua coleção em ordem, dentro da única transação que carrega a escrita:

Request ──► BEGIN ──► beforeSave/beforeDelete ──► DB operation ──► afterSave/afterDelete ──► COMMIT ──► Response

Todo hook é aguardado, e um erro lançado por qualquer um deles desfaz a escrita inteira:

  1. beforeSave, beforeDelete rodam antes da escrita. Eles podem alterar os valores de entrada, validar ou recusar a requisição lançando um erro — quem chamou recebe 400 CALLBACK_REJECTED e nada é escrito.
  2. afterSave, afterDelete rodam depois da escrita, mas antes do commit. Um erro reverte a linha e responde o mesmo 400, com details.stage indicando o hook. Eles mantêm a transação aberta enquanto rodam, por isso são o lugar errado para um webhook ou qualquer outra chamada de rede — Hooks explica para onde vai esse trabalho.
Método Caminho Autenticação Descrição
GET /health e /api/health nenhuma Verificação de vivacidade/prontidão (liveness/readiness check)
GET /api/docs nenhuma; schema:read quando enableSwagger é false (o padrão do runtime em produção) A especificação OpenAPI 3.0 em JSON
GET /api/swagger nenhuma Swagger UI. Servido apenas fora de produção; REBASE_ENABLE_SWAGGER não consegue ativá-lo em produção. Não é servido quando enableSwagger é false, nem em desenvolvimento
GET /api/meta/schema-version nenhuma O hash do schema a partir do qual este backend foi construído, e a versão do runtime — deliberadamente não autenticado, e nada que descreva o schema
GET /api/meta/contract schema:read: um admin, a chave de serviço ou uma chave de API que o tenha O contrato completo da coleção, para rebase generate-sdk --from. Comportamento fail-closed: 404 quando nenhuma autenticação está configurada
GET /metrics REBASE_METRICS_TOKEN quando definido Métricas do Prometheus, quando REBASE_METRICS=true

A especificação OpenAPI é gerada automaticamente a partir das definições das suas coleções: ela descreve os endpoints de listagem, consulta, contagem, agregação, leitura, criação, atualização, exclusão e operações em lote de cada coleção atendida pelo backend, suas rotas aninhadas e _batch, com seus parâmetros de consulta e esquemas de resposta. Não se trata de um mapa completo de toda a superfície HTTP — as rotas de auth, storage, functions e cron estão documentadas apenas neste site — e colunas marcadas como excludeFromApi são omitidas dela.

Para uma coleção com softDelete, a especificação diz que uma exclusão, individual ou em lote, move a linha para a lixeira, e documenta ?hard=true nas duas exclusões e ?deleted=include|only na listagem, na leitura individual, na contagem e na agregação. O explorador de API do Studio envia apenas os parâmetros que a especificação nomeia, então é isso que permite a ele expurgar uma linha ou listar a lixeira.

Chamadores automatizados (machine callers) se autenticam com uma chave com escopo específico em vez de uma sessão: Chaves de API.

O schema completo das coleções do projeto — cada coleção, propriedade e relação — é servido a um administrador autenticado:

GET /api/meta/contract

Ele requer o escopo schema:read, que um admin tem, e em uma implantação sem autenticação configurada ele não é disponibilizado de forma alguma (404 CONTRACT_UNAVAILABLE), evitando expor o schema publicamente. Seu endpoint irmão retorna uma string de versão que representa o schema sem descrevê-lo, sendo deliberadamente acessível sem credenciais — que é o que um job de CI consulta periodicamente:

GET /api/meta/schema-version

Para o formato dos endpoints em vez do schema por trás deles, o documento OpenAPI fica em GET /api/docs, com o Swagger UI em /api/swagger fora de produção.