MRProjetos Manual de Utilização — Protheus REST API v1.0 · Jun 2025

Manual de Utilização — Protheus REST API

API REST sobre o ERP Protheus/TOTVS · Node.js + Express + TypeScript

Node.js Express TypeScript MSSQL MySQL Protheus / TOTVS

1. Introdução e Visão Geral

A Protheus REST API é uma solução desenvolvida em Node.js + Express + TypeScript que expõe qualquer tabela do ERP Protheus/TOTVS como um endpoint REST completo — sem necessidade de código adicional para cada entidade do banco de dados.

A API conecta-se diretamente ao banco de dados do Protheus (SQL Server ou MySQL), interpreta as requisições HTTP e gera as queries SQL correspondentes de forma automática, respeitando as convenções do Protheus como o soft-delete via coluna D_E_L_E_T_ e a chave primária R_E_C_N_O_.

Características principais

  • Exposição automática de qualquer tabela Protheus via REST
  • Suporte a MSSQL (SQL Server) e MySQL
  • Operações CRUD individuais e em lote (bulk)
  • Operações bulk atômicas com rollback automático em caso de falha
  • Soft-delete nativo compatível com o padrão Protheus
  • Múltiplos ambientes de banco de dados configuráveis
  • Sistema completo de filtros, ordenação, paginação e relacionamentos
  • Autenticação JWT via header HTTP
  • Controle de acesso por endereço IP
Nota
A API nunca remove registros fisicamente do banco. Toda operação de deleção executa um soft-delete compatível com o Protheus, garantindo integridade dos dados históricos.

2. Pré-requisitos e Instalação

2.1 Pré-requisitos

ComponenteVersão mínimaObservação
Node.jsv14 LTSRecomendado v16 ou superior
npmv6Incluído com Node.js
SQL Server2012+Para plataforma mssql
MySQL5.7+Para plataforma mysql
TypeScriptv4.3Instalado via devDependencies

2.2 Instalação

bash
# 1. Instale as dependências
npm install

# 2. Compile o TypeScript para a pasta server/
npm run build

# 3. Inicie o servidor
npm run serve

2.3 Scripts npm

ScriptComandoDescrição
buildtscCompila TypeScript → server/
dev / servenodemon serverInicia com hot-reload
watchtsc -wCompila em modo watch (sem iniciar)
Dica
Em produção, ajuste o campo appConfigFilePath no package.json para apontar para config/protheus-rest-api.config.jsonc. Por padrão ele aponta para o arquivo de teste.

2.4 Estrutura de diretórios

protheus-rest-api/
├── config/
│   ├── protheus-rest-api.config.jsonc        # Config de produção
│   └── protheus-rest-api.config-test.jsonc   # Config de teste
├── rdbconfig.json                            # Config da biblioteca rdb-server
├── src/                                      # Fonte TypeScript
│   ├── index.ts
│   └── libs/protheus-sql-server/
│       └── libs/rdb-server/
│           ├── controller.js
│           ├── db-manager.js
│           ├── query-generator.js
│           ├── query-support.js
│           ├── routes.js
│           └── db-connection/
├── server/                                   # Build compilado (npm run build)
└── docs/

3. Configuração Detalhada

3.1 protheus-rest-api.config.jsonc

Arquivo principal de configuração. O arquivo utilizado é definido pela chave appConfigFilePath do package.json.

protheus-rest-api.config.jsonc
{
  "service": {
    "name":        ".3001 - Node Protheus REST API",
    "description": "Protheus REST API",
    "script":      "server"
  },
  "port": 3001,
  "serverConfig": {
    "logDateWhenStarting":  true,
    "allowOnlyAddresses": ["::ffff:127.0.0.1", "::1"]
  },
  "completeLogger":  false,
  "bulkMaxRecords": 100,
  "environments": [
    {
      "name":      "protheus-prd",
      "plataform": "mssql",
      "config": {
        "user":              "seu_usuario",
        "password":          "sua_senha",
        "server":            "ip_ou_hostname",
        "database":          "PROTHEUS_PRD",
        "connectionTimeout": 999999999,
        "requestTimeout":    999999999,
        "options": { "enableArithAbort": true }
      },
      "default": true
    }
  ]
}
PropriedadeTipoDescrição
portnumberPorta HTTP em que o servidor irá escutar
serverConfig.allowOnlyAddressesstring[]IPs autorizados. Outros IPs recebem 401
serverConfig.logDateWhenStartingbooleanExibe data/hora em cada entrada de log
completeLoggerbooleantrue para registrar queries SQL completas no console
bulkMaxRecordsnumberLimite máximo de registros por requisição bulk (padrão: 100)
environments[].namestringIdentificador do ambiente, enviado no header environment
environments[].plataformstring"mssql" para SQL Server ou "mysql" para MySQL
environments[].defaultbooleanAmbiente usado quando o header environment não é enviado
environments[].configobjectCredenciais e parâmetros de conexão com o banco

3.2 rdbconfig.json

Configuração interna da biblioteca rdb-server. Define o comportamento padrão para todas as tabelas.

rdbconfig.json
{
  "rdbConfig": {
    "ENVIRONMENT_REQUEST_LOCATION": "headers",
    "envDefaults": {
      "idColumnName":             "R_E_C_N_O_",
      "deletedColumnName":        "D_E_L_E_T_",
      "deletedValue":             "'*'",
      "deleteAdd":               ["R_E_C_D_E_L_ = R_E_C_N_O_"],
      "columnsNotShow":          ["R_E_C_D_E_L_"],
      "updateAdd":               [],
      "insertAdd":               [],
      "idAutoIncrement":          false,
      "relationshipConditionById": false
    }
  }
}
PropriedadePadrãoDescrição
idColumnNameR_E_C_N_O_Coluna de chave primária usada em operações por ID
deletedColumnNameD_E_L_E_T_Coluna que indica registros deletados
deletedValue'*'Valor atribuído à coluna de deleção no soft-delete
deleteAdd["R_E_C_D_E_L_ = R_E_C_N_O_"]Expressões SQL adicionais na deleção
columnsNotShow["R_E_C_D_E_L_"]Colunas ocultadas nos resultados de GET
updateAdd[]Expressões SQL adicionais em todo UPDATE
insertAdd[]Expressões SQL adicionais em todo INSERT
idAutoIncrementfalseQuando false, ID calculado via MAX(R_E_C_N_O_)+1
relationshipConditionByIdfalseQuando false, $embed/$expand exigem $on explícito

3.3 Configurando múltiplos ambientes

MSSQL (SQL Server)

mssql
{
  "name":      "protheus-prd",
  "plataform": "mssql",
  "config": {
    "user":     "usuario",
    "password": "senha",
    "server":   "192.168.1.10",
    "database": "PROTHEUS",
    "options": {
      "enableArithAbort": true
    }
  },
  "default": true
}

MySQL

mysql
{
  "name":      "protheus-hml",
  "plataform": "mysql",
  "config": {
    "user":     "usuario",
    "password": "senha",
    "host":     "192.168.1.20",
    "database": "PROTHEUS_HML"
  },
  "default": false
}

4. Autenticação

Todas as requisições devem incluir um token JWT no header Authorization. O ambiente é selecionado pelo header environment.

HTTP Headers
Authorization: Bearer <seu-token-jwt>
Content-Type:  application/json
environment:   protheus-prd
HeaderObrigatórioDescrição
AuthorizationSimToken JWT no formato Bearer <token>
Content-TypePOST/PUTDeve ser application/json
environmentNãoNome do ambiente. Se omitido, usa o ambiente com "default": true
Aviso
Requisições de IPs não listados em allowOnlyAddresses recebem 401 Unauthorized imediatamente, antes de qualquer validação de token.

5. Endpoints CRUD Padrão

Cada tabela Protheus é acessível pelo nome como parâmetro de rota. O :id corresponde ao valor de R_E_C_N_O_.

5.1 GET — Listar registros

GET /:tableName

Retorna array JSON com todos os registros não deletados. Suporta filtros e paginação (ver Seção 7).

Requisição
GET /SBM?BM_FILIAL=01&$limit=20
Authorization: Bearer <token>
environment:   protheus-prd
Resposta — 200 OK
[
  { "R_E_C_N_O_": 105, "BM_FILIAL": "01", "BM_GRUPO": "A001", "BM_DESC": "Grupo A" },
  { "R_E_C_N_O_": 106, "BM_FILIAL": "01", "BM_GRUPO": "A002", "BM_DESC": "Grupo B" }
]

O header de resposta x-total-length informa o total de registros sem paginação.

5.2 GET — Buscar por ID

GET /:tableName/:id

Retorna o registro com R_E_C_N_O_ = :id. Retorna 404 se não encontrado ou deletado.

Resposta — 200 OK
{ "R_E_C_N_O_": 105, "BM_FILIAL": "01", "BM_GRUPO": "A001", "BM_DESC": "Grupo A" }

5.3 POST — Inserir registro único

POST /:tableName body = objeto JSON

Insere um único registro. Body deve ser um objeto JSON (não array). Retorna o registro inserido com 201.

Requisição
POST /SBM
{ "BM_FILIAL": "01", "BM_GRUPO": "A003", "BM_DESC": "Grupo C" }
Resposta — 201 Created
{ "R_E_C_N_O_": 107, "BM_FILIAL": "01", "BM_GRUPO": "A003", "BM_DESC": "Grupo C" }

5.4 PUT — Atualizar registro único

PUT /:tableName/:id

Atualiza o registro identificado por :id. Somente os campos no body são atualizados. Retorna o registro atualizado.

Requisição
PUT /SBM/105
{ "BM_DESC": "Nova Descrição" }

5.5 DELETE — Soft-delete

DELETE /:tableName/:id

Executa soft-delete (ver Seção 8). O registro não é removido fisicamente. Retorna 204 No Content.

6. Bulk Operations

Operações em lote permitem inserir, atualizar ou deletar múltiplos registros em uma única requisição. Todas são atômicas: se qualquer registro falhar, toda a transação é revertida (rollback) e nenhuma alteração é persistida.

Limite de registros
O máximo por requisição bulk é definido por bulkMaxRecords no config (padrão: 100). Acima desse limite retorna 413 Payload Too Large.

6.1 POST — Inserção em lote (bulk insert)

POST /:tableName body = array JSON

Quando o body é um array, a API detecta automaticamente a operação bulk.

Requisição
POST /SBM
[
  { "BM_FILIAL": "01", "BM_GRUPO": "A001", "BM_DESC": "Grupo 1" },
  { "BM_FILIAL": "01", "BM_GRUPO": "A002", "BM_DESC": "Grupo 2" }
]
Resposta — 201 Created
{ "inserted": 2 }

6.2 PUT — Atualização em lote (bulk update)

PUT /:tableName sem :id · cada item deve ter R_E_C_N_O_
Requisição
PUT /SBM
[
  { "R_E_C_N_O_": 105, "BM_DESC": "Atualizado 1" },
  { "R_E_C_N_O_": 106, "BM_DESC": "Atualizado 2" }
]
Resposta — 200 OK
{ "updated": 2 }

6.3 DELETE — Soft-delete em lote

DELETE /:tableName body = array de IDs (R_E_C_N_O_)
Requisição
DELETE /SBM
[105, 106, 107]
Resposta — 200 OK
{ "deleted": 3 }

6.4 Contratos de Erro — Bulk Operations

HTTPSituaçãoBody de resposta
400Array vazio{ "error": "Array não pode estar vazio" }
400Body não é array{ "error": "Body deve ser um array" }
413Acima do limite{ "error": "Máximo de 100 registros por requisição bulk" }
422Falha em item + rollback{ "error": "Operação revertida...", "failed_at": { "index": N, "reason": "..." } }
422Erro SQL + rollback{ "error": "Operação revertida...", "reason": "mensagem SQL" }
Atenção — Atomicidade
O rollback reverte todos os registros do lote. Use failed_at.index para identificar o item problemático no array (base 0).

7. Filtros e Query Parameters

Os endpoints GET /:tableName aceitam um conjunto rico de parâmetros de query string para filtrar, projetar, ordenar, paginar e relacionar dados. Parâmetros prefixados com $ são diretivas de controle; os demais são interpretados como filtros de coluna.

7.1 Filtros de coluna — igualdade exata

Qualquer parâmetro sem prefixo $ e sem sufixo de operador é interpretado como filtro de igualdade na coluna correspondente. As comparações de texto são case-insensitive e acentos são normalizados automaticamente.

Exemplos
# Filtro simples
GET /SBM?BM_FILIAL=01

# Múltiplos filtros (AND implícito)
GET /SBM?BM_FILIAL=01&BM_GRUPO=A001

7.2 Operadores de comparação

Para filtros além da igualdade exata, sufixe o nome da coluna com o operador: COLUNA$operador=valor

Operadores disponíveis · Sintaxe: COLUNA$operador=valor
(padrão / =)

Igualdade. SQL: UPPER(COLUNA) = UPPER(valor)

?BM_GRUPO=A001
$gt

Maior que. SQL: COLUNA > valor

?R_E_C_N_O_$gt=100
$gte

Maior ou igual. SQL: COLUNA >= valor

?R_E_C_N_O_$gte=100
$lt

Menor que. SQL: COLUNA < valor

?R_E_C_N_O_$lt=200
$lte

Menor ou igual. SQL: COLUNA <= valor

?R_E_C_N_O_$lte=200
$ne

Diferente de. SQL: COLUNA != valor

?BM_GRUPO$ne=A001
$like

Contém. SQL: UPPER(COLUNA) LIKE UPPER('%valor%')

?BM_DESC$like=grupo
$nlike

Não contém. SQL: UPPER(COLUNA) NOT LIKE UPPER('%valor%')

?BM_DESC$nlike=cancelado
$in

Em lista. SQL: COLUNA IN (v1, v2, v3)

Separe os valores com |.

?BM_GRUPO$in=A001|A002|A003
$nin

Fora da lista. SQL: COLUNA NOT IN (v1, v2)

Separe os valores com |.

?BM_GRUPO$nin=A001|A002

Valores especiais nos filtros

Valor na query stringConvertido para
$nullnull
$undefinedundefined
$truetrue (1)
$falsefalse (0)

7.3 $map — Projeção de colunas

Seleciona apenas as colunas especificadas no resultado. Sem $map, retorna todas as colunas. R_E_C_N_O_ é sempre incluído automaticamente.

Formatos aceitos
Lista simples

Colunas separadas por vírgula.

?$map=BM_GRUPO,BM_DESC
Com alias

Formato coluna alias (separados por espaço). O alias se torna a chave no JSON de retorno.

?$map=BM_GRUPO grupo,BM_DESC descricao
Exemplo com alias
GET /SBM?$map=BM_GRUPO grupo,BM_DESC descricao
# Retorna: [{ "R_E_C_N_O_": 105, "grupo": "A001", "descricao": "Grupo A" }]

7.4 $sort — Ordenação

Define a ordem dos resultados. Sem $sort, a API ordena por R_E_C_N_O_.

Formatos aceitos
Crescente (ASC)
?$sort=BM_GRUPO
Decrescente (DESC)

Prefixe com -.

?$sort=-BM_GRUPO
Múltiplas colunas

Separadas por vírgula.

?$sort=BM_FILIAL,-BM_GRUPO

7.5 $page / $limit — Paginação por página

Paginação clássica. $page começa em 1. O valor padrão de $limit é 10. O header de resposta x-total-length retorna o total de registros sem paginação.

Exemplos
# Página 1, 20 registros por página
GET /SBM?$page=1&$limit=20

# Página 3, 50 por página, com filtro e ordenação
GET /SBM?BM_FILIAL=01&$page=3&$limit=50&$sort=BM_GRUPO

7.6 $start / $end — Paginação por intervalo de linhas

Alternativa à paginação por página. Define o intervalo exato de linhas (numeradas após aplicar filtros e ordenação). A linha 1 é o primeiro resultado.

Exemplos
# Linhas 1 a 50
GET /SBM?$start=1&$end=50

# A partir da linha 51 (sem limite final)
GET /SBM?$start=51
Nota
$start/$end e $page/$limit são mutuamente exclusivos. Use apenas uma forma por requisição.

7.7 q — Busca full-text

Realiza busca em todas as colunas da tabela simultaneamente. Case-insensitive. Em MSSQL usa collation específico para suporte a acentos. Colunas numéricas são convertidas para texto antes da comparação.

Exemplos
# Busca "embalagem" em todos os campos
GET /SBM?q=embalagem

# Combinado com filtro de coluna
GET /SBM?BM_FILIAL=01&q=materia
Aviso de performance
A busca full-text gera uma condição OR para cada coluna da tabela. Em tabelas largas pode impactar a performance. Prefira filtros por coluna específica quando possível.

7.8 $query — Expressão SQL literal

Permite passar uma expressão SQL WHERE arbitrária. Útil para condições complexas não cobertas pelos operadores padrão. A expressão é envolta em parênteses e adicionada ao WHERE via AND.

Exemplo
GET /SBM?$query=BM_GRUPO LIKE 'A%' AND BM_DESC NOT IN ('Inativo', 'Obsoleto')
Atenção — Segurança
$query injeta SQL diretamente na query. Use apenas em integrações controladas onde a entrada é previamente validada.

7.9 $embed — Relacionamentos embarcados (1:N)

Embarca registros de uma tabela filha em cada registro do resultado principal. Relação pai → N filhos. O resultado da tabela filha aparece como array no registro pai, sob a chave com o nome da tabela filha.

Exemplo — pedidos com seus itens
GET /SC5?$embed=SC6&$on=SC6.C6_NUM = SC5.C5_NUM

# Resultado:
[
  {
    "R_E_C_N_O_": 10,
    "C5_NUM": "000001",
    "SC6": [
      { "C6_ITEM": "01", "C6_PRODUTO": "PROD001" },
      { "C6_ITEM": "02", "C6_PRODUTO": "PROD002" }
    ]
  }
]

7.10 $expand — Expansão de referência (N:1)

Expande uma referência de chave estrangeira trazendo o registro pai completo. Relação filho → 1 pai. O registro pai aparece como objeto no registro filho.

Exemplo — itens de pedido com produto expandido
GET /SC6?$expand=SB1&$on=SB1.B1_COD = SC6.C6_PRODUTO

# Resultado:
[
  {
    "R_E_C_N_O_": 50,
    "C6_PRODUTO": "PROD001",
    "SB1": { "B1_DESC": "Produto 001", "B1_UM": "UN" }
  }
]

7.11 $on — Condição de JOIN

Define a condição SQL de relacionamento usada por $embed e $expand. Obrigatório quando relationshipConditionById é false (padrão). Aceita expressões SQL padrão: tabela.coluna = tabela.coluna.

Resumo de todos os parâmetros de query

ParâmetroTipoDescriçãoExemplo
COLUNA=valorstringFiltro de igualdade na coluna?BM_FILIAL=01
COLUNA$gtanyMaior que?R_E_C_N_O_$gt=100
COLUNA$gteanyMaior ou igual?R_E_C_N_O_$gte=100
COLUNA$ltanyMenor que?R_E_C_N_O_$lt=200
COLUNA$lteanyMenor ou igual?R_E_C_N_O_$lte=200
COLUNA$neanyDiferente de?BM_GRUPO$ne=A001
COLUNA$likestringContém (LIKE %valor%)?BM_DESC$like=grupo
COLUNA$nlikestringNão contém (NOT LIKE)?BM_DESC$nlike=inativo
COLUNA$instringEm lista (separar com |)?BM_GRUPO$in=A001|A002
COLUNA$ninstringFora da lista (separar com |)?BM_GRUPO$nin=A001|A002
qstringBusca full-text em todas as colunas?q=embalagem
$querystringExpressão SQL WHERE literal?$query=B1_TIPO='PA'
$mapstringProjeção de colunas, com suporte a alias?$map=BM_GRUPO,BM_DESC
$sortstringOrdenação. Prefixar com - para DESC?$sort=-BM_GRUPO
$pagenumberNúmero da página (começa em 1)?$page=2
$limitnumberRegistros por página (padrão: 10)?$limit=50
$startnumberLinha inicial para intervalo de linhas?$start=1
$endnumberLinha final (usar com $start)?$end=50
$embedstringEmbarca tabela filha (1:N) no resultado?$embed=SC6&$on=...
$expandstringExpande referência para tabela pai (N:1)?$expand=SB1&$on=...
$onstringCondição SQL de JOIN para $embed/$expand?$on=SC6.C6_NUM=SC5.C5_NUM

8. Soft Delete — Comportamento Protheus

O Protheus utiliza "deleção lógica" (soft-delete) baseada em colunas de controle. Esta API implementa esse comportamento nativamente e de forma transparente.

SQL gerado pelo DELETE

SQL
UPDATE SBM
SET    D_E_L_E_T_   = '*',
       R_E_C_D_E_L_ = R_E_C_N_O_
WHERE  R_E_C_N_O_   = 105
AND    D_E_L_E_T_   != '*'

Colunas de controle

ColunaConfiguraçãoComportamento
D_E_L_E_T_deletedColumnNameRecebe '*' quando o registro é deletado
R_E_C_D_E_L_deleteAddRecebe o valor de R_E_C_N_O_ (oculto no GET por padrão)
R_E_C_N_O_idColumnNameChave primária em todas as operações por ID

Todos os GET adicionam automaticamente WHERE D_E_L_E_T_ != '*'. Registros deletados nunca aparecem nos resultados sem intervenção direta no banco.

Aviso
A coluna R_E_C_D_E_L_ é excluída dos resultados pelo array columnsNotShow do rdbconfig.json. Para torná-la visível, remova-a da lista.

9. Códigos de Retorno HTTP

CódigoStatusQuando ocorre
200OKGET com sucesso; PUT individual com sucesso (retorna registro); DELETE bulk com sucesso
201CreatedPOST com sucesso. Individual: retorna objeto inserido. Bulk: retorna { "inserted": N }
204No ContentDELETE individual com sucesso. Sem body na resposta.
400Bad RequestBody inválido, array vazio em bulk, ou body obrigatório ausente
401UnauthorizedIP de origem não está em allowOnlyAddresses
404Not FoundRegistro com :id não existe ou foi deletado
413Payload Too LargeArray bulk acima de bulkMaxRecords
422Unprocessable EntityFalha em operação bulk. Rollback executado. Body contém detalhes.
500Internal Server ErrorErro não tratado ou falha de conexão com o banco

10. Exemplos Práticos — Tabela SBM

A tabela SBM (Grupos de Materiais) é usada como referência. Substitua por qualquer tabela Protheus disponível.

Listar com filtro, ordenação e paginação

GET /SBM?BM_FILIAL=01&$page=1&$limit=10&$sort=BM_GRUPO
Authorization: Bearer <token>
environment:   protheus-prd

Projeção de colunas com alias

GET /SBM?BM_FILIAL=01&$map=BM_GRUPO grupo,BM_DESC descricao&$sort=BM_GRUPO

Busca full-text + intervalo de R_E_C_N_O_

GET /SBM?q=embalagem&R_E_C_N_O_$gte=100&R_E_C_N_O_$lte=500

Filtro com lista ($in) e exclusão ($nlike)

GET /SBM?BM_GRUPO$in=MA01|PA01|EM01&BM_DESC$nlike=obsoleto

Inserção em lote

POST /SBM
[
  { "BM_FILIAL": "01", "BM_GRUPO": "MA01", "BM_DESC": "Matéria Prima"  },
  { "BM_FILIAL": "01", "BM_GRUPO": "PA01", "BM_DESC": "Produto Acabado" },
  { "BM_FILIAL": "01", "BM_GRUPO": "EM01", "BM_DESC": "Embalagem"       }
]

Atualização em lote

PUT /SBM
[
  { "R_E_C_N_O_": 200, "BM_DESC": "Matéria Prima Nacional"     },
  { "R_E_C_N_O_": 201, "BM_DESC": "Produto Acabado Exportação" }
]

Deleção em lote

DELETE /SBM
[200, 201, 202]

11. Troubleshooting

Erros de conexão

ConnectionError — Failed to connect

Causa: Banco inacessível, credenciais incorretas ou IP/porta errados.

Solução: Verifique server, user, password e database no config. Confirme que o banco está ativo e acessível a partir do servidor Node.js.

RequestError — Timeout expired

Causa: Query demorou mais que requestTimeout.

Solução: Aumente requestTimeout no config, adicione índices ou refine os filtros.

Outros erros comuns

SintomaCausa provávelSolução
Toda requisição retorna 401IP não listado em allowOnlyAddressesAdicionar o IP ao array no config e reiniciar
Dados do ambiente erradoHeader environment ausente ou com valor incorretoVerificar o campo name exato no environments
POST retorna 400Body inválido ou Content-Type ausenteVerificar headers e estrutura do body
PUT retorna 404R_E_C_N_O_ inexistente ou registro deletadoConfirmar o ID com um GET antes do PUT
Bulk retorna 422Falha em um registro (constraint, tipo, etc.)Verificar failed_at.index e failed_at.reason
Bulk retorna 413Array acima de bulkMaxRecordsReduzir o lote ou aumentar bulkMaxRecords
Serviço não inicia: porta em usoOutro processo usando a mesma portaAlterar a porta no config ou encerrar o processo conflitante
Módulo não encontradoBuild não gerado (server/ vazio)Executar npm run build
$embed/$expand sem resultado$on não informadoIncluir $on com a condição de JOIN correta

Ativar log completo de queries

protheus-rest-api.config.jsonc
// Temporariamente: ativa o log das queries SQL completas no console
"completeLogger": true
Aviso
Mantenha completeLogger: false em produção. O log completo pode expor dados sensíveis.