Using
REST API
applies to v0.5.1LuraDB’s interface is a RESTful API. There is no proprietary protocol and no driver stack to install: every language and standard tool can talk to it, and the API is browsable down to the individual record. This page covers what all endpoints share: where they live, how a client authenticates, what an error looks like, how conditional requests and paging work. The endpoint pages generated from the OpenAPI document of the release say the rest.
The API is HTTP with JSON bodies
Section titled “The API is HTTP with JSON bodies”The examples in this handbook use a server on http://127.0.0.1:3000, the default address, and the admin key in $KEY. The paths fall into a few families:
| Path | What it is |
|---|---|
/health |
The heartbeat. It needs no key. |
/version |
The version handshake: the server version and the version of the API contract. |
/store-api/domains, /store-api/json/domains, /store-api/rel/domains |
Create, list and delete domains, one set per engine. |
/store-api/kv/{domain}/… |
The key-value engine, see Key-value. |
/store-api/json/{domain}/… |
The JSON engine, see JSON documents. |
/store-api/rel/{domain}/… |
The relational engine, see LuraSQL. |
/store-api/auth/… |
Users, permissions and keys, see Security. |
/store-api/backups, /store-api/restores/{id} |
Backup and restore, see Backup and restore. |
/store-api/metrics, /store-api/config, /store-api/events, /store-api/logs |
Operation, see Monitoring. |
curl -s http://127.0.0.1:3000/version -H "Authorization: Bearer $KEY"{ "server_version": "0.5.1", "api_version": "0.14.2" }The api_version is the version of the contract. What a change of it means is on Compatibility.
A request body is JSON, with one exception: the value of a key-value entry is raw bytes. An endpoint that reads JSON wants the header Content-Type: application/json; without it the answer is 415, and a body that is JSON but has the wrong shape is a 422:
curl -si -X POST http://127.0.0.1:3000/store-api/json/domains \ -H "Authorization: Bearer $KEY" -d '{"name": "orders"}'HTTP/1.1 415 Unsupported Media Typecontent-type: text/plain; charset=utf-8
Expected request with `Content-Type: application/json`A body may be up to 2 MiB; larger ones answer 413, with higher limits for the bulk loads (see Limits and defaults). Names in a path are percent-encoded: a key user/42/profile is written user%2F42%2Fprofile.
The port and the address come from server.port and server.bind_address. The same API is available over HTTPS and a Unix socket, see Security.
Every request carries an API key
Section titled “Every request carries an API key”With authentication on, which is the packaged default, a client sends its key as a Bearer token. Only GET /health and, while server.hello_enabled is on, GET / work without one:
curl -s http://127.0.0.1:3000/store-api/auth/whoami -H "Authorization: Bearer $KEY"curl -si http://127.0.0.1:3000/store-api/kv/default/keys/hello -H "Authorization: Bearer lura_0000"{ "name": "admin", "role": "Admin" }HTTP/1.1 401 Unauthorizedcontent-type: text/plain; charset=utf-8
Unauthorized401 means the key is missing or unknown; 403 means the key is valid but has no right for the request. An admin key passes every check; a user key reaches the domains it has a permission on, with the level read, write or ddl, separately for each engine:
curl -si -X PUT http://127.0.0.1:3000/store-api/kv/sessions/keys/s-1 \ -H "Authorization: Bearer $USER_KEY" -d xHTTP/1.1 403 Forbiddencontent-type: text/plain; charset=utf-8
ForbiddenHere $USER_KEY is the key of a user that may only read sessions. How to create users, grant permissions, rotate keys and turn the setting auth.enabled off for a test on a loopback address is on Security.
An error is a status and a line of text
Section titled “An error is a status and a line of text”An error answers with a plain-text body and the status in the response line. Read the status, not the text: the text is for people and can change between releases. A few statuses ask the client to do something:
| Status | Meaning | What to do |
|---|---|---|
400, 422 |
The request is malformed or breaks a rule. | Fix the request; sending it again does not help. |
401, 403 |
No valid key, or no right. | Check the key and its permissions. |
404, 410 |
Not found, or the domain is being deleted. | |
409 |
The request collides with what exists: a name, a primary key, a link target. | |
412, 428 |
A precondition failed or is required. | Read again and retry with the new ETag, see below. |
429 |
The domain used up its budget of reads or writes per second. | Wait for Retry-After. |
503, 507 |
A queue is full, or a volume or the memory is short. The request did nothing. | Wait for Retry-After and send it again unchanged. |
Every status LuraDB uses, with its typical cause and the headers that go with it, is on Status codes.
curl -si http://127.0.0.1:3000/store-api/kv/default/keys/nope -H "Authorization: Bearer $KEY"HTTP/1.1 404 Not Foundcontent-type: text/plain; charset=utf-8content-length: 35
404 Not Found: key 'nope' not foundConditional requests stop lost updates
Section titled “Conditional requests stop lost updates”Every key and every document carries an ETag. A write with If-Match succeeds only if the key is still at the version you read, and a write with If-None-Match: * succeeds only if the key does not exist yet. A failed precondition answers 412 with the current ETag; a domain created with require_conditional_writes answers 428 to a write that carries neither header.
curl -si -X PUT http://127.0.0.1:3000/store-api/kv/sessions/keys/s-4f2a \ -H "Authorization: Bearer $KEY" -H "If-None-Match: *" -d 'replaced'HTTP/1.1 412 Precondition Failedcontent-type: text/plain; charset=utf-8etag: "1a1a3944e160000"
412 Precondition Failed: the key's current version does not satisfy the write preconditionThe rules, the exceptions for bulk operations and what a reader sees are on Consistency and conditional writes.
Lists are paged with limit and offset
Section titled “Lists are paged with limit and offset”Every endpoint that lists takes limit and offset. There are no cursors: the order of a list is fixed (by key, or by primary key), so offset walks through it, and a write between two requests can move an entry across a page boundary. The answer carries the numbers that were applied, so a limit above the maximum is visible, and total counts the matches before paging.
| List | Parameters | Default and maximum | Answer |
|---|---|---|---|
GET /store-api/kv/{domain}/keys |
prefix, contains, limit, offset |
1000, 10000 | keys, total, offset, limit |
GET /store-api/json/{domain}/documents |
limit, offset, keys_only |
50, 1000 | documents, keys, total, offset, limit |
POST /store-api/json/{domain}/search |
limit, offset in the body |
50, 1000 | documents, total, offset, limit |
GET /store-api/rel/{domain}/tables/{table}/rows |
limit, offset, column=value |
1000, rel.max_limit |
rows, row_count, limit, offset, limit_applied |
POST /store-api/rel/{domain}/sql |
LIMIT and OFFSET in the statement |
1000 without LIMIT, rel.max_limit |
rows, row_count, limit_applied |
With three keys in the domain default, the second page of size one and a request for more than the maximum:
for i in 1 2 3; do curl -s -X PUT http://127.0.0.1:3000/store-api/kv/default/keys/item-$i \ -H "Authorization: Bearer $KEY" -d $idonecurl -s "http://127.0.0.1:3000/store-api/kv/default/keys?prefix=item-&limit=1&offset=1" \ -H "Authorization: Bearer $KEY"curl -s "http://127.0.0.1:3000/store-api/kv/default/keys?prefix=item-&limit=100000" \ -H "Authorization: Bearer $KEY"{ "keys": ["item-2"], "total": 3, "offset": 1, "limit": 1 }{ "keys": ["item-1", "item-2", "item-3"], "total": 3, "offset": 0, "limit": 10000 }The second request asked for 100000 and got the maximum, 10000. In the relational answers, limit_applied is true when more matching rows existed than were returned.
The endpoint reference is generated from the contract
Section titled “The endpoint reference is generated from the contract”The group Endpoints in the sidebar, below this page, is generated from api/openapi.json of the release, so it cannot drift from the server. Each endpoint has a page with its parameters, its request body, its answers with the status codes and headers, and the condition for each status. The overview lists every tag, and the sidebar groups the pages by the tags of the contract:
| Tag | Covers |
|---|---|
| Key-Value Store | Keys, values, metadata, null state, counting, deleting by prefix, watching. |
| JSON Document Store | Documents, search, bulk load, export. |
| JSON Indexes | Index definitions and re-indexing. |
| Relational Store | The /sql endpoint. |
| Relational Browse, Relational Rows, Relational Import | Tables, views, rows and the CSV and TSV import. |
| Domains, Relational Domains | Creating, listing, reading and deleting domains, key-value and relational. The JSON domain endpoints are under JSON Document Store. |
| Auth | Users, permissions, key rotation, whoami. |
| Backup | Backups and restores. |
| Metrics, Logs, Events | Health, metrics, configuration, log files and the event stream. |
One operation of each engine shows what such a page holds: put_key writes a key-value entry, put_document writes a JSON document, and execute_sql runs a LuraSQL statement.
The create, list, read and delete operations of domains have the same operation name in all three engines. The generated pages carry one page for each name, that of the relational engine; the paths of the key-value and the JSON variant are in the table at the top of this page and in the contract served by the running server.
The same contract is served by the running server at /api-docs/openapi.json, and as a Swagger UI, when server.swagger_enabled is on, see First five minutes.