API as DB Specification
This document specifies the HTTP contract that an external API must implement to be used as a data source by the Express REST API and MCP Server Framework’s API-as-DB feature. If you are building a custom API (without using this framework’s generate command to create the proxy) and want it to work as a “database” behind the framework’s proxy, your API must comply with this contract.
Purpose and audience
This specification is for:
- Implementers who build their own REST API (in any language or stack) and want it to be consumed by this framework as an API-as-DB backend.
- Integrators who need to verify that an existing API is compatible before connecting it via
npm run generate -- --name X --baseurl <URL> --endpoint <path>.
If your API satisfies the contract below, the framework can proxy all CRUD and list operations to it, and the generated tests can be used to verify compliance.
Contract summary
- Base URL + endpoint: The framework calls
{baseurl}/{endpoint}for the collection and{baseurl}/{endpoint}/{id}for a single resource. Example:https://api.example.com+products→https://api.example.com/products. - Response envelope: Every successful response body must be a JSON object. It must include a
dataproperty (array for list, object for single resource or create/update/delete). For list (GET collection), the response must also includetotalResult(number) so the framework can support count/pagination. - Record identity: Each record must expose a unique identifier as
_idorid(string or number). The framework uses whichever is present for updates and deletes. - Headers: The framework sends an
x-tagheader on every request (value from the app’sAPI_DB_Keyconfig). If your backend is another instance of this framework, it will requirex-tagon POST; custom APIs may accept or ignore it.
Endpoints reference
| Operation | HTTP | URL | Request | Response |
|---|---|---|---|---|
| List | GET | /{endpoint} |
Query params (see below) | { data: [], totalResult: number } |
| Get one | GET | /{endpoint}/{id} |
— | { data: {} } |
| Create | POST | /{endpoint} |
Body = document | { data: {} } |
| Update one | PATCH | /{endpoint}/{id} |
Body = partial document | { data: {} } |
| Update many | PUT | /{endpoint} |
Query params + body | { data: {} } |
| Delete one | DELETE | /{endpoint}/{id} |
— | { data: {} } |
| Delete many | DELETE | /{endpoint} |
Query params | { data: {} } |
Example request/response (list)
Request:
GET /products?limit=2&sort=-_id HTTP/1.1
Host: api.example.com
x-tag: <value-from-framework>
Response:
{
"data": [
{ "_id": "507f1f77bcf86cd799439011", "name": "Widget A", "createdAt": "2025-01-15T10:00:00.000Z" },
{ "_id": "507f1f77bcf86cd799439012", "name": "Widget B", "createdAt": "2025-01-14T09:00:00.000Z" }
],
"totalResult": 42
}
Example request/response (get one)
Request:
GET /products/507f1f77bcf86cd799439011 HTTP/1.1
Host: api.example.com
Response:
{
"data": { "_id": "507f1f77bcf86cd799439011", "name": "Widget A", "createdAt": "2025-01-15T10:00:00.000Z" }
}
Example request/response (create)
Request:
POST /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
x-tag: <value-from-framework>
{"name": "New Widget", "price": 9.99}
Response:
{
"data": { "_id": "507f1f77bcf86cd799439013", "name": "New Widget", "price": 9.99, "createdAt": "2025-01-16T12:00:00.000Z" }
}
Query parameters (list / GET collection)
The framework may send these as query parameters on GET {baseurl}/{endpoint}. Your API should accept and honor the ones it supports:
| Parameter | Description |
|---|---|
limit |
Page size (number). |
sort |
Sort spec, e.g. -_id (descending by _id) or name (ascending by name). |
select |
Comma-separated field names for projection (e.g. name,price). |
populate |
Related fields to populate (string). |
from |
Start of date range (ISO string; maps from internal createdAt.$gt). |
to |
End of date range (ISO string; maps from internal createdAt.$lt). |
lastId |
Cursor for next page: the _id or id of the last item from the previous page. |
search |
Search string when the list is used as search. |
Other query keys may be filter criteria (e.g. name=foo). The framework does not send skip; pagination is cursor-based via lastId.
Optional: schema discovery
For MCP and schema metadata, the framework can discover your API’s schema from:
GET {baseurl}/{endpoint}/schema
Return a JSON Schema object with at least a properties object (and optionally required, description). Example:
{
"type": "object",
"properties": {
"id": { "type": "string", "description": "Unique identifier" },
"name": { "type": "string", "description": "Product name" },
"createdAt": { "type": "string", "format": "date-time" }
},
"required": ["id", "name"]
}
If this endpoint is missing or returns an invalid response, the framework falls back to the _schema defined in the generated API model file.
Error handling
On failure, the framework uses error.response?.data || error. Your API can return standard HTTP status codes (4xx, 5xx) with a JSON body; that body will be propagated to the framework’s error handling.
Testing for compliance
Using the generated unit tests (recommended)
Compliance is verified by running the generated API-based unit tests against your API:
- Generate an API-as-DB service that points at your API:
npm run generate -- --name MyProxy --baseurl https://your-api.example.com --endpoint resources - Start your API so it is reachable at the given base URL (or use a stub server that implements this spec).
- Run the test suite:
npm testIf your API runs on a different URL/port than the default, set
TEST_API_BASEURL:TEST_API_BASEURL=http://localhost:3000 npm test
The generated controller and route tests call your API and assert response shape (data, totalResult), status codes, and CRUD behavior. If these tests pass, your API satisfies the contract.
Optional: manual checklist
If you are not using the framework’s generate command to create the proxy service, you can verify compliance with this checklist:
- GET collection returns a JSON object with
data(array) andtotalResult(number). - GET
/{endpoint}/{id}returns a JSON object withdata(single object). - POST to collection accepts a JSON body and returns
{ data: <created object> }; records include_idorid. - PATCH to
/{endpoint}/{id}accepts a JSON body and returns{ data: <updated object> }. - PUT to collection (with query params and body) returns
{ data: ... }. - DELETE to
/{endpoint}/{id}returns{ data: ... }. - DELETE to collection (with query params) returns
{ data: ... }. - Each record has
_idoridfor identity. - (Optional) GET
/{endpoint}/schemareturns a JSON Schema object withproperties.
You can use curl or any REST client to run through these points against your base URL and endpoint.