Mirajv1.0
EN

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:

FamilyPathsUse
Endpointspaths 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)
SQLPOST /api/sqlFree-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)ArgumentDefaultPurpose
rest--rest ON|OFFOFFOpens the endpoint or not
rest_port--rest-port <port>7009TCP port, distinct from port and mcp_port; 0: chosen by the system, displayed at startup
rest_bind--rest-bind <address>127.0.0.1Listening address; outside the loopback interface, TLS is mandatory
rest_endpoints--rest-endpoints ON|OFFONEndpoints family open on the server
rest_tables--rest-tables ON|OFFOFFTables family open on the server
rest_sql--rest-sql ON|OFFOFFSQL family open on the server
rest_basic--rest-basic ON|OFFONLogin by account and password (Authorization: Basic) accepted
rest_basic_access--rest-basic-access <families>ENDPOINTSFamilies allowed for password logins: ALL, NONE, or ENDPOINTS, TABLES, SQL separated by commas
rest_idle_timeout--rest-idle-timeout <s>60Persistent HTTP connection closed after this delay without a request
rest_statement_timeout--rest-statement-timeout <s>30Maximum duration of a statement (fraction allowed; 0: no limit)
rest_max_rows--rest-max-rows <n>1000Maximum rows returned by a response (1 to 1,000,000)
rest_cors_origins--rest-cors-origins <origins>emptyOrigins 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.pem

Always 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#

SituationMessage or effect
Endpoint openMIRAJ <version> <edition> - REST endpoint on 127.0.0.1:7009, access ENDPOINTS. (, TLS with a certificate)
rest = OFF and a rest_* variable givenNote, server started, REST port closed
rest_port equal to port or to mcp_portServer stopped
rest_bind outside the loopback interface without a certificateServer 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:

FormHeaderWhen to use it
API tokenAuthorization: Bearer mja_…Applications and services: revocable, limited to families, endpoints and databases
Account and passwordAuthorization: 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, Expires

CREATE 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#

StepRefusal
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 account403 (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 /api and /mcp are reserved. Error 9045 otherwise.
  • Two endpoints cannot answer the same method on a path of the same shape (GET /clients/{id} and GET /clients/{code}), across all databases: error 9044. A fixed segment takes precedence over a parameter (/clients/nouveaux before /clients/{id}).
  • A known path called with another method: 405 with the Allow header.

23.4.3 Parameters#

  • Declared in PARAMS (name type [DEFAULT value]); an undeclared path parameter is a VARCHAR(255).
  • The body reads a parameter as :name, never by its bare name: WHERE id = :id compares the id column to the id parameter.
  • Values are taken, by name, from the path ({id}), the query string (?ville=Oran) and, for POST, PUT and PATCH, 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)#

RETURNSResponse
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
ONE200, object of the first row of the last result; 404 without a row
NONE204 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 ROUTINE on the database; dropping it: ALTER ROUTINE.
  • Calling it: EXECUTE on 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 GRANTS writes GRANT EXECUTE ON ENDPOINT `gestion`.`fiche_client` TO …; information_schema.ENDPOINT_PRIVILEGES lists 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.

RequestEffect
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}?filtersModifies the filtered rows (at least one filter)
DELETE /api/{database}/{table}/{key}Deletes the row; 204, 404 without it
DELETE /api/{database}/{table}?filtersDeletes 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#

PathPurpose
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).

StatusCase
400Invalid JSON, missing or unknown parameter (9046), invalid SQL (1064), unknown column (1054), value outside the type
401Authentication missing or refused; Basic over plain HTTP outside the loopback interface
403Blocked address, family or endpoint not allowed (9047), SQL rights (1044, 1142, 1143, 1370), token management by token (9034)
404Family closed on the server, unknown path, row absent (ONE, key), unknown database or table
405Method not served on this path (Allow)
409Duplicate key (1062), foreign key (1451, 1452), lock conflict (1205, 1213)
503max_connections reached (1040)
504Statement timeout exceeded (1969) or statement interrupted (1317)

23.9 Best practices#

  • Prefer endpoints: the application holds only EXECUTE on 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 with DROP API TOKEN.
  • Keep rest_tables and rest_sql closed 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_origins to the origins of your applications.

See also#