23. REST API: Miraj over HTTP and HTTPS
The REST endpoint of miraj-server opens the databases to any application that speaks HTTP: web or mobile application, script, automation tool, other service. It requires neither a driver nor a client library: just HTTP requests and JSON.
It serves three access families, opened separately on the server and for each client:
| Family | Paths | Use |
|---|---|---|
| Endpoints | paths that you define (GET /clients/{id}) | APIs written in SQL (CREATE ENDPOINT): the application calls only what you have planned, with typed parameters |
| Tables | /api/{database}/{table}[/{key}] | Read and write the rows of a table (filters, sorting, pagination) |
| SQL | POST /api/sql | Free-form SQL, for a trusted internal tool |
Application (web, mobile, script)
│ HTTP or HTTPS: GET https://host:7009/clients/42
│ Authorization: Bearer mja_… (or Basic account:password)
▼
┌─────────────────────────────── miraj-server ───────────────────────────────┐
│ port 7007: Miraj network protocol │
│ port 7009: REST endpoint (--rest ON) │
│ family open on the server? ─► client authenticated? │
│ family allowed for the token? ─► SQL rights of the account (table, │
│ column, EXECUTE on the endpoint) ─► SQL statement ─► locks, triggers, │
│ foreign keys, journal, transactions, replication │
└────────────────────────────────────────────────────────────────────────────┘Each request opens a Miraj session under the client's account, executes an ordinary SQL statement, then closes the session: nothing is ever written directly to the data files, and the account's SQL rights always apply, down to the column (chapter 10).
23.1 Editions concerned#
The REST endpoint exists in all editions, Express included.
23.2 Activation and configuration#
23.2.1 Variables#
Like the main port: default value, then miraj_config.xml, then command line (the command line takes precedence, see 11.2). Disabled by default.
Variable (miraj_config.xml) | Argument | Default | Purpose |
|---|---|---|---|
rest | --rest ON|OFF | OFF | Opens the endpoint or not |
rest_port | --rest-port <port> | 7009 | TCP port, distinct from port and mcp_port; 0: chosen by the system, displayed at startup |
rest_bind | --rest-bind <address> | 127.0.0.1 | Listening address; outside the loopback interface, TLS is mandatory |
rest_endpoints | --rest-endpoints ON|OFF | ON | Endpoints family open on the server |
rest_tables | --rest-tables ON|OFF | OFF | Tables family open on the server |
rest_sql | --rest-sql ON|OFF | OFF | SQL family open on the server |
rest_basic | --rest-basic ON|OFF | ON | Login by account and password (Authorization: Basic) accepted |
rest_basic_access | --rest-basic-access <families> | ENDPOINTS | Families allowed for password logins: ALL, NONE, or ENDPOINTS, TABLES, SQL separated by commas |
rest_idle_timeout | --rest-idle-timeout <s> | 60 | Persistent HTTP connection closed after this delay without a request |
rest_statement_timeout | --rest-statement-timeout <s> | 30 | Maximum duration of a statement (fraction allowed; 0: no limit) |
rest_max_rows | --rest-max-rows <n> | 1000 | Maximum rows returned by a response (1 to 1,000,000) |
rest_cors_origins | --rest-cors-origins <origins> | empty | Origins allowed by CORS: empty (none), *, or https://app.example.com,https://… |
A family that is closed on the server does not exist for clients: its paths answer 404, even to an ACCESS ALL token.
23.2.2 Examples#
<miraj>
<rest>ON</rest>
<rest_bind>0.0.0.0</rest_bind>
<tls_cert>C:\miraj\cert.pem</tls_cert>
<tls_key>C:\miraj\key.pem</tls_key>
<rest_cors_origins>https://app.exemple.com</rest_cors_origins>
</miraj>miraj-server.exe --root D:\miraj\data --log --rest ON
miraj-server.exe --root D:\miraj\data --log --rest ON --rest-tables ON --rest-sql ON --rest-port 8443 --rest-bind 0.0.0.0 --tls-cert cert.pem --tls-key key.pemAlways start the server with --log: every request is then recorded (REST GET /clients/42 -> 200, token mobile, account app@%, 3 ms, from 10.0.0.5).
23.2.3 Startup and configuration errors#
| Situation | Message or effect |
|---|---|
| Endpoint open | MIRAJ <version> <edition> - REST endpoint on 127.0.0.1:7009, access ENDPOINTS. (, TLS with a certificate) |
rest = OFF and a rest_* variable given | Note, server started, REST port closed |
rest_port equal to port or to mcp_port | Server stopped |
rest_bind outside the loopback interface without a certificate | Server stopped: provide --tls-cert and --tls-key (the same as for the main port) |
With a certificate, all connections to the endpoint are encrypted (HTTPS).
23.3 Authentication#
Every request (except GET /api/health) presents one of the two forms:
| Form | Header | When to use it |
|---|---|---|
| API token | Authorization: Bearer mja_… | Applications and services: revocable, limited to families, endpoints and databases |
| Account and password | Authorization: Basic base64(account:password) | Administration tools, one-off scripts. Refused over plain HTTP outside the loopback interface (401 "Basic authentication requires HTTPS"); families limited by rest_basic_access |
Authentication failures are counted per address, as on the main port: increasing delay, then the address is blocked at max_connect_errors (FLUSH HOSTS unblocks it).
23.3.1 API tokens#
CREATE API TOKEN name [FOR account]
[ACCESS { ALL | family [, family …] }] -- default: ACCESS ENDPOINTS
[DATABASES (database, …)]
[EXPIRE { NEVER | INTERVAL n DAY }];
-- family := ENDPOINTS [([database.]name, …)] | TABLES | SQL
ALTER API TOKEN name ACCESS …; -- families replaced, secret unchanged, effective from the next request
DROP API TOKEN [IF EXISTS] name;
SHOW API TOKENS; -- Name, User, Host, Access, Databases, Created, ExpiresCREATE API TOKEN returns the token's secret (mja_…) only once: only its fingerprint is kept, in the accounts vault (replicated in a cluster). Examples:
-- Mobile application: only the APIs intended for it
CREATE API TOKEN mobile FOR 'app'@'%' ACCESS ENDPOINTS;
-- Partner: two specific endpoints, 90 days
CREATE API TOKEN partenaire FOR 'partenaire'@'%' ACCESS ENDPOINTS (ventes.catalogue, ventes.stock) EXPIRE INTERVAL 90 DAY;
-- Internal tool: tables and free-form SQL on one database
CREATE API TOKEN outil FOR 'dev'@'%' ACCESS TABLES, SQL DATABASES (recette);A token grants no rights: it restricts those of the account. Its allowed family, then the account's SQL privileges (EXECUTE on the endpoint, SELECT on the table or column…) are both checked. A session opened by token never manages tokens (9034).
Creating a token for your own account is free; for another account, CREATE USER is required.
23.3.2 Order of checks#
| Step | Refusal |
|---|---|
Family open on the server (rest_endpoints, rest_tables, rest_sql) | 404 |
| Client authenticated (valid, unexpired token; correct password; account not locked) | 401 |
Family allowed for the token (ACCESS) or for password logins (rest_basic_access) | 403, error 9047 |
Endpoint allowed for the token (ACCESS ENDPOINTS (…)) | 403, error 9047 |
| SQL rights of the account | 403 (1044, 1142, 1143, 1370…) |
23.4 Endpoints: APIs written in SQL#
An endpoint associates an HTTP method and path with a SQL body (a statement, or a BEGIN … END block like a procedure). This is the recommended way to open up data: the application has access only to the planned operations, with typed parameters, and can hold only EXECUTE on its endpoints.
CREATE [OR REPLACE] [DEFINER = account] ENDPOINT [IF NOT EXISTS] [database.]name
{ GET | POST | PUT | PATCH | DELETE } '/path/{param}/…'
[PARAMS (name type [DEFAULT value], …)]
[RETURNS { ROWS | ONE | NONE | AFFECTED }]
[COMMENT 'text'] [SQL SECURITY { DEFINER | INVOKER }]
AS statement | BEGIN … END;
DROP ENDPOINT [IF EXISTS] [database.]name;
SHOW ENDPOINTS [FROM database] [LIKE 'pattern'];
SHOW CREATE ENDPOINT [database.]name;23.4.1 Complete example#
USE gestion;
-- Customer record: one row, 404 if it does not exist
CREATE ENDPOINT fiche_client GET '/clients/{id}' PARAMS (id INT) RETURNS ONE
AS SELECT id, nom, ville FROM clients WHERE id = :id;
-- Filtered list, optional parameter
CREATE ENDPOINT liste_clients GET '/clients' PARAMS (ville VARCHAR(40) DEFAULT NULL, limite INT DEFAULT 50)
AS SELECT id, nom, ville FROM clients WHERE :ville IS NULL OR ville = :ville ORDER BY nom LIMIT :limite;
-- Creation: 201 and inserted rows
CREATE ENDPOINT nouveau_client POST '/clients' PARAMS (nom VARCHAR(80), ville VARCHAR(40)) RETURNS AFFECTED
AS INSERT INTO clients (nom, ville) VALUES (:nom, :ville);
-- Multi-statement processing
CREATE ENDPOINT solder_facture POST '/factures/{id}/solder' PARAMS (id INT) RETURNS ONE
AS BEGIN
DECLARE reste DECIMAL(12,2);
SELECT montant - paye INTO reste FROM factures WHERE id = :id FOR UPDATE;
UPDATE factures SET paye = montant WHERE id = :id;
INSERT INTO reglements (facture, montant) VALUES (:id, reste);
SELECT :id AS facture, reste AS regle;
END;
-- The application only has the right to call these endpoints
CREATE USER 'app'@'%' IDENTIFIED BY '…';
GRANT EXECUTE ON ENDPOINT gestion.fiche_client TO 'app'@'%';
GRANT EXECUTE ON ENDPOINT gestion.liste_clients TO 'app'@'%';
GRANT EXECUTE ON ENDPOINT gestion.nouveau_client TO 'app'@'%';
CREATE API TOKEN mobile FOR 'app'@'%';GET /clients/42 → 200 {"id": 42, "nom": "Ali", "ville": "Oran"}
GET /clients?ville=Oran&limite=10 → 200 [{"id": 42, …}, …]
POST /clients {"nom": "Lina", "ville": "Alger"} → 201 {"affected_rows": 1, "last_insert_id": 43}
POST /factures/7/solder → 200 {"facture": 7, "regle": 120.50}23.4.2 Path and method#
- The path starts with
/; its segments are fixed (letters, digits,-,_,.,~) or{name}parameters. Paths under/apiand/mcpare reserved. Error 9045 otherwise. - Two endpoints cannot answer the same method on a path of the same shape (
GET /clients/{id}andGET /clients/{code}), across all databases: error 9044. A fixed segment takes precedence over a parameter (/clients/nouveauxbefore/clients/{id}). - A known path called with another method: 405 with the
Allowheader.
23.4.3 Parameters#
- Declared in
PARAMS (name type [DEFAULT value]); an undeclared path parameter is aVARCHAR(255). - The body reads a parameter as
:name, never by its bare name:WHERE id = :idcompares theidcolumn to theidparameter. - Values are taken, by name, from the path (
{id}), the query string (?ville=Oran) and, forPOST,PUTandPATCH, the keys of a JSON object body. Those from the path and the query string are strings converted to the declared type; those from the body keep their JSON type (null, number, boolean, string; bytes{"hex": "…"}or{"base64": "…"}; geometry{"wkt": "POINT(1 2)"}). - A parameter without a value or
DEFAULT, or a value for an unknown parameter: 400, error 9046. The same name given twice (path and query string, for example): 400.
23.4.4 Response (RETURNS)#
RETURNS | Response |
|---|---|
ROWS (default) | 200, array of objects from the last result of the body ([] without a result); beyond rest_max_rows, rows are truncated and the X-Miraj-Truncated: true header is set |
ONE | 200, object of the first row of the last result; 404 without a row |
NONE | 204 without a body |
AFFECTED | {"affected_rows": n, "last_insert_id": id}; 201 for POST, 200 otherwise |
Values are written in JSON as by the MCP endpoint: exact decimals, integers beyond 2^53 as a string, dates and times as SQL text, bytes as a hexadecimal string "0x…", geometries as an object {"wkt": "POINT(1 2)", "srid": 0}.
23.4.5 Rights#
- Creating an endpoint:
CREATE ROUTINEon the database; dropping it:ALTER ROUTINE. - Calling it:
EXECUTEon the endpoint (GRANT EXECUTE ON ENDPOINT database.name TO account) or on its database (GRANT EXECUTE ON database.* …, which opens all its endpoints); otherwise 403, error 1370. SQL SECURITY DEFINER(default): the body runs with the rights of its definer; the caller needs no rights on the tables.SQL SECURITY INVOKER: with those of the caller, columns included (1143 for a denied column).SHOW GRANTSwritesGRANT EXECUTE ON ENDPOINT `gestion`.`fiche_client` TO …;information_schema.ENDPOINT_PRIVILEGESlists them.
Endpoints are neither procedures nor functions: CALL does not find them, information_schema.ROUTINES does not show them; they appear in SHOW ENDPOINTS and information_schema.ENDPOINTS. They are backed up with the database (BACKUP, miraj-dump) and replicated in a cluster like procedures.
23.5 Tables: /api/{database}/{table}#
Family opened by rest_tables = ON and by the token (ACCESS TABLES). Each request becomes a parameterized SQL statement under the account's rights.
| Request | Effect |
|---|---|
GET /api/{database}/{table} | Rows (100 by default, at most rest_max_rows) |
GET /api/{database}/{table}/{key} | The row with primary key key (404 without it); multi-column key: values separated by commas, in the order of the table's columns |
POST /api/{database}/{table} | Inserts a JSON object, or an array of objects with the same keys; 201 |
PATCH /api/{database}/{table}/{key} | Modifies the columns of the body; 200 and affected_rows |
PATCH /api/{database}/{table}?filters | Modifies the filtered rows (at least one filter) |
DELETE /api/{database}/{table}/{key} | Deletes the row; 204, 404 without it |
DELETE /api/{database}/{table}?filters | Deletes the filtered rows (at least one filter) |
Read parameters: select=a,b; order=a.desc,b; limit; offset; filters column[.op]=value, with op among eq (default), ne, gt, ge, lt, le, like, in (comma-separated values), null (true or false). Example: GET /api/gestion/clients?ville=Oran&nom.like=A%25&order=nom&limit=20.
Without select, the response contains the columns the account can read: all of them if it has SELECT on the table, otherwise only those granted to it. Requesting, filtering, sorting or writing a denied column: 403, error 1143.
23.6 Free-form SQL: POST /api/sql#
Family opened by rest_sql = ON and by the token (ACCESS SQL).
{ "sql": "SELECT id, nom FROM gestion.clients WHERE ville = :ville", "params": { "ville": "Oran" }, "max_rows": 100 }params: array for ? placeholders, object for :name ones. Response:
{ "results": [ { "columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(40)"}],
"rows": [[42, "Ali"]], "row_count": 1, "truncated": false,
"affected_rows": null, "last_insert_id": null } ],
"warnings": [] }Several statements separated by ; produce several results. On error, the response carries error and the results of the statements already executed. Each request is a new session: a transaction does not span requests (use a statement block, or an endpoint).
23.7 Other paths#
| Path | Purpose |
|---|---|
GET /api/health | {"status": "ok", "version": "…", "edition": "…"}, without authentication (availability probe) |
OPTIONS … | CORS preflight response for an origin in rest_cors_origins |
23.8 Errors#
Body: {"error": {"code": 1143, "sqlstate": "42000", "message": "…"}} (code and sqlstate are null for an HTTP error without a SQL code).
| Status | Case |
|---|---|
| 400 | Invalid JSON, missing or unknown parameter (9046), invalid SQL (1064), unknown column (1054), value outside the type |
| 401 | Authentication missing or refused; Basic over plain HTTP outside the loopback interface |
| 403 | Blocked address, family or endpoint not allowed (9047), SQL rights (1044, 1142, 1143, 1370), token management by token (9034) |
| 404 | Family closed on the server, unknown path, row absent (ONE, key), unknown database or table |
| 405 | Method not served on this path (Allow) |
| 409 | Duplicate key (1062), foreign key (1451, 1452), lock conflict (1205, 1213) |
| 503 | max_connections reached (1040) |
| 504 | Statement timeout exceeded (1969) or statement interrupted (1317) |
23.9 Best practices#
- Prefer endpoints: the application holds only
EXECUTEon its APIs, the SQL stays on the server, and a schema change affects only the endpoint bodies. - One account per application, one token per deployment (
ACCESS ENDPOINTS (…)as narrow as possible,EXPIRE), revoked withDROP API TOKEN. - Keep
rest_tablesandrest_sqlclosed in production, or reserved for internal-tool tokens limited to one database. - Outside the loopback interface, the endpoint requires TLS; place it behind a firewall and limit
rest_cors_originsto the origins of your applications.
See also#
- Chapter 10: Accounts and privileges: column privileges,
EXECUTEper endpoint. - Chapter 20: MCP server: same HTTP transport, for AI assistants.
- Chapter 15: Error codes: 1143, 9044 to 9047.