Skip to content

REST API

dAvePi mounts a per-schema Express router for every loaded schema. The shape is identical across resources — once you know it for account, you know it for everything else.

MethodPathPurpose
POST/api/v1/<path>Create. userId / accountId stamped from JWT. Optional Idempotency-Key header.
GET/api/v1/<path>List. Pagination + filter + sort + __include + q + __includeDeleted.
PUT/api/v1/<path>Bulk upsert. Filter + record body; ownership pinned to JWT.
GET/api/v1/<path>/:idRead one. Same __include set as list.
PUT/api/v1/<path>/:idPartial update. State-machine fields validated against transitions[current].
DELETE/api/v1/<path>/:idSoft-delete (or hard, with softDelete: false).
POST/api/v1/<path>/:id/restoreClear deletedAt. Soft-delete schemas only.
GET/api/v1/<path>/:id/historyAudit log for the record, newest first. Audit-enabled schemas only.
GET/api/v1/<path>/aggregations/<name>Run a declared aggregation.
POST/api/v1/<path>/:id/<file-field>Upload (multipart).
GET/api/v1/<path>/:id/<file-field>Fetch metadata + URL.
DELETE/api/v1/<path>/:id/<file-field>Delete the blob and clear the meta.
GET/api/v1/<path>-schemaThe framework-introspection JSON for this schema.

Bearer JWT on every authenticated route:

Authorization: Bearer <token>

Token is issued by POST /login (see Quickstart). The framework reads user_id and roles from the verified token; clients never supply either.

Three knobs on every list endpoint:

ParamDefaultDescription
__page11-indexed page number.
__sortcreatedAt:desc (or score when q is set)field:asc / field:desc / score. Multiple comma-separated.
__perPagePAGE_SIZE env (default 20)Capped at 200.

Response shape:

{
"results": [/* docs */],
"totalResults": 142,
"page": 1,
"perPage": 20,
"totalPages": 8,
"nextPage": 2,
"prevPage": null
}

Filters use mongo-querystring conventions on the URL — the same operators Mongo accepts, JSON-encoded for object values:

GET /api/v1/contact?accountId=abc
GET /api/v1/contact?createdAt={"$gte":"2026-01-01"}
GET /api/v1/contact?name={"$regex":"^Ja","$options":"i"}
OperatorForm
Equalityfield=value
Comparisonfield={"$gt": ...} (or $gte, $lt, $lte, $ne)
In listfield={"$in": ["a","b"]}
Regexfield={"$regex": "...", "$options": "i"}

Sub-objects MUST be JSON-encoded (URL-safe). The runtime in the typed client does this for you.

GET /api/v1/account/abc?__include=contacts,primaryContact

Comma-separated relation names from the schema’s relations map. See Relations.

GET /api/v1/contact?q=jane

Available on schemas with at least one searchable: true field. Default sort becomes score. See Search.

GET /api/v1/account?__includeDeleted=true

Returns tombstoned rows as well. Defaults to false. Relations ignore this flag — they always filter tombstones. See Soft delete.

POST /api/v1/account
Idempotency-Key: 9f3c-...

See Idempotency keys.

Every typed error returns the same shape:

{
"error": {
"code": "INVALID_TRANSITION",
"message": "Cannot transition status from 'review' to 'archived'",
"details": {
"current": "review",
"attempted": "archived",
"allowed": ["approved", "rejected"]
}
}
}

Codes are stable contracts. See Errors.

Two limiters mount at boot:

PathDefaultOverride
/api/*600 req/min/IPRATE_LIMIT_API_PER_MIN
/login, /register10 req/min/IPRATE_LIMIT_AUTH_PER_MIN

Both are skipped when NODE_ENV=test so the suite isn’t tripped. Rate-limited responses are 429 with { error: { code: 'RATE_LIMITED' } }.

Configured via CORS_ORIGINS (comma-separated allowlist). With no value, no cross-origin requests are accepted — set it to http://localhost:3000,https://app.example.com for the typical admin SPA + production frontend setup.

Live at /api-docs. The JSON spec is at /api-docs/swagger.json — useful as a Swagger 2.0 export when an external tool needs it. For agent-friendly introspection, prefer _describe — same data, smaller envelope, first-class relations and state machines.