rest-client
Call external REST APIs from Mendix — the three approaches (inline REST CALL, consumed REST client document, generated from OpenAPI) and how to choose. Use when integrating with any external REST API.
SKILL.md.claude/skills/mendix/rest-client/SKILL.md
GitHub<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->
<!-- /skill-index -->
Change an Existing Client —
Find it fast
sed -n '<from>,<to>p' SKILL.md one section rather than reading the file;
./mxcli syntax <topic> answers how do I write this for a fraction of either.
| lines | section |
|---|---|
| 29-44 | Three Approaches |
| 45-110 | Approach 0: OpenAPI Import (Fastest) |
| 111-337 | Approach 1: REST Client (Manual) |
| 338-383 | Approach 2: REST CALL (Inline HTTP) |
| 384-416 | Data Transformers (JSLT — Mendix 11.9+) |
| 417-454 | JSON Structures & Mappings |
| 455-526 | Complete Pipeline Example |
| 527-557 | Published REST: Authentication Parameters |
| 558-567 | Mendix Validation Rules |
| 568-588 | BSON Types Reference |
REST Integration Skill
Use this skill when integrating with external REST APIs from Mendix.
Three Approaches
Mendix offers three ways to call REST APIs from microflows. Choose based on the use case:
| Approach | When to Use | Artifacts |
|---|---|---|
| OpenAPI import | API has an OpenAPI 3.0 spec — auto-generate from the spec | REST client document generated in one command |
| REST Client (manual) | No spec available, or need fine-grained control | REST client document + microflow |
| REST CALL (inline) | One-off calls, quick prototyping, dynamic URLs, low-level HTTP control | Microflow only |
Both REST Client approaches can be combined with Data Transformers (Mendix 11.9+) and Import/Export Mappings to map between JSON and entities.
No API to call against yet — or one you would rather not depend on while building? mock-rest-apis covers standing up an endpoint you control and pointing the app at it.
Approach 0: OpenAPI Import (Fastest)
If the API has an OpenAPI 3.x spec (JSON or YAML), generate the REST client in one command.
A Swagger 2.0 spec ("swagger": "2.0", e.g. petstore.swagger.io/v2/swagger.json) is
refused — convert it first (npx swagger2openapi in.json -o out.json, or
https://converter.swagger.io) and import the converted file:
mdl 1;
-- From a local file (relative to the .mpr file)
create or modify consumed rest service CapitalModule.CapitalAPI (
OpenAPI: 'specs/capital.json'
);
-- From a URL
create or modify consumed rest service PetStoreModule.PetStoreAPI (
OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json'
);
-- Override the base URL (replaces servers[0].url from the spec)
create or modify consumed rest service PetStoreModule.PetStoreStaging (
OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json',
BaseUrl: 'https://staging.petstore.example.com/api/v3'
);
This generates:
- All operations with correct HTTP method, path, parameters, headers, body, and response type
- Resource groups based on OpenAPI
tags - Basic auth if the spec declares it at the top level
- The spec stored inside the document for Studio Pro parity
BaseUrl is optional. When omitted, servers[0].url from the spec is used — but only if it is
absolute. A relative server URL (/api/v3) cannot be a BaseUrl: the import warns
("server URL … is relative and cannot be used as BaseUrl") and leaves the client without one,
which fails at call time rather than at import time. Set BaseUrl explicitly in that case.
When provided, BaseUrl overrides the spec's value — useful when the spec points at production
but you need to import against staging, a different version, or a local mock
(see mock-rest-apis).
Preview without writing:
describe contract operation from openapi 'specs/capital.json';
After import: the REST client is ready to use with SEND REST REQUEST. No manual operation definition needed.
But every JSON response is imported as response: json — a raw string, not objects. The
import does not generate response entities yet (ako/mxcli#1234). To get objects, create the
non-persistent entities yourself and give the operations you call a mapping (see Response
Types below):
mdl 1;
alter consumed rest service PetStoreModule.PetStoreAPI
modify operation getPetById (
response: mapping PetStoreModule.Pet { Id = id, Name = name }
);
Writing the entities and an import-mapping document but leaving the operation at
response: json is the common mistake: nothing uses the mapping, and nothing reports that.
Approach 1: REST Client (Manual)
Define the API once as a REST client document, then call its operations from microflows.
Step 1 — Create the REST Client
mdl 1;
create consumed rest service Module.OpenMeteoAPI (
BaseUrl: 'https://api.open-meteo.com/v1',
authentication: none
)
{
operation GetForecast (
method: get,
path: '/forecast',
query: ($latitude: decimal, $longitude: decimal, $current: string),
headers: ('Accept': 'application/json'),
timeout: 30,
response: json as $WeatherJson
)
operation PostData (
method: post,
path: '/submit',
headers: ('Content-Type': 'application/json'),
body: json from $JsonPayload,
response: none
)
};
A header value is a template, like the path: {Name} is the operation
parameter Name, which must be declared in Parameters: (CE7056 otherwise).
parameters: ($Token: string),
headers: ('Authorization': 'Bearer {Token}')
'Bearer ' + $Token is the old spelling of the same header (MDL-DEPR711);
mxcli fmt --upgrade rewrites it. It used to store only Bearer .
Authentication
-- No authentication
authentication: none
-- Basic auth
authentication: basic (username: 'user', password: 'secret')
Body Types
-- JSON variable (Mendix expression stored on the operation)
body: json from $JsonPayload
-- String template with parameter placeholders
body: template '{ "name": "{name}", "value": {value} }'
-- Export mapping (entity → JSON, aligned with export mapping syntax)
body: mapping Module.RequestEntity {
name = Name,
email = Email,
}
There is no binary/file body here. A consumed operation's body is one of
Rest$JsonBody, Rest$StringBody or Rest$ImplicitMappingBody, so a file
document has nowhere to go. body: file from $Doc is refused as MDL-REST02
— it used to be written as a string body holding the literal text $Doc, which
returns HTTP 200 with a 4-byte payload.
Upload binary from a microflow instead, which does have a binary body:
call rest service post 'https://api.example.com/upload' (
Headers: ('ContentType': 'application/pdf'),
Body: binary $Doc/Contents,
) returns response;
response: file as $Doc on an operation is unaffected — downloads work.
Response Types
-- Simple types. The variable is optional and is NOT stored — the call site
-- binds it — so `describe` prints the bare kind (`response: json`), and that
-- form runs again as written.
response: json
response: json as $Result
response: string as $text
response: file as $Document
response: status as $Code
response: none
-- Import mapping (JSON → entity, aligned with import mapping syntax)
response: mapping Module.ResponseEntity {
"Id" = "id",
"status" = "status",
create Module.Items_Response/Module.Item = items {
"Sku" = "sku",
"Quantity" = "quantity",
}
}
mapping takes an ENTITY plus a body — never a mapping document. The name
after mapping is the target entity, and the { ... } body lists the JSON
fields; Mendix stores the result inline on the operation
(Rest$ImplicitMappingResponseHandling). A consumed REST operation has nowhere
to put a reference to a standalone import/export mapping — the metamodel defines
only the inline handler and "no response handling". So this is wrong, and is
now rejected by mxcli check as MDL-REST01:
response: mapping Module.IMM_Something -- ✗ names a mapping document, no body
If you already have an import mapping document you want to reuse, call it from
the microflow instead — send rest request with response: json as $Raw, then
$Obj = import from mapping Module.IMM_Something($Raw).
Reaching a nested JSON member
A member several levels down is written with /, exactly as in a mapping
document — you do NOT need an entity for the levels in between:
response: mapping Module.FlatProbe {
"ItemId" = "id",
"Title" = "fields/Title", -- reaches {"fields": {"Title": …}}
}
Mendix stores that as the pipe path (Object)|fields|Title. Before mxcli
0.19.x the inline serializer appended the text verbatim, producing
(Object)|fields/Title — one member with a slash in its name — which passed
mxcli check, exec, mx check and describe, and returned an empty
column at runtime with no diagnostic. If you are on an older build, give each
level its own entity instead.
An export body's nested elements
Body: MAPPING is an EXPORT mapping, and its nested elements must not be
"create". mxcli writes them correctly, but it is worth knowing what a wrong one
looks like, because mxbuild does not report it as a check error — it throws
while loading the project, so the app cannot be opened at all:
System.AggregateException: … (Export Object Mappings cannot have
ObjectHandling set to 'Create')
Query parameters carry no type in the Mendix model: Rest$QueryParameter stores
a name only. MDL still requires $name: Type for the sake of the grammar, but
the type is dropped at write time, so describe re-emits every query parameter
as String.
Step 2 — Call from a Microflow
mdl 1;
create microflow Module.ACT_GetWeather ()
returns Module.WeatherInfo as $Weather
begin
-- Call the REST client operation
send rest request Module.OpenMeteoAPI.GetForecast;
-- Extract response body from system variable
declare $RawJson string = $latestHttpResponse/content;
-- (Optional) Transform with JSLT
$SimplifiedJson = transform $RawJson with Module.SimplifyWeather;
-- Import into entity
$Weather = import from mapping Module.IMM_Weather($SimplifiedJson);
return $Weather;
end;
CRITICAL: After send rest request, the response is in $latestHttpResponse (System.HttpResponse):
$latestHttpResponse/content— response body (String)$latestHttpResponse/StatusCode— HTTP status (Integer)
Show / Describe / Drop
list consumed rest services [in module];
describe consumed rest service Module.ClientName;
drop consumed rest service Module.ClientName;
create or modify consumed rest service Module.ClientName ... -- idempotent; REPLACES the operation list
Change an Existing Client — alter, Not create or modify
create or modify declares the client's whole operation list: an operation it
does not list is deleted (the output names it). To add, change or remove
one operation — above all on a client made in Studio Pro, whose operations carry
settings MDL cannot express — use alter. Every operation it does not name is
written back exactly as stored.
mdl 1;
alter consumed rest service Module.BookingApi
add operation GetBooking (
method: get,
path: '/bookings/{id}',
parameters: ($id: string),
response: json
),
modify operation PostBooking (timeout: 60), -- only the keys listed change
drop operation LegacyBooking,
set (BaseUrl: 'https://api.example.com/v2'); -- BaseUrl, Authentication
add refuses a name that exists (use modify); modify and drop refuse one
that does not, listing the operations there are. Actions apply in order, so
drop operation X, add operation X (…) replaces X whole.
Approach 2: REST CALL (Inline HTTP)
Call an HTTP endpoint directly from a microflow — no REST client document needed. Best for one-off calls, dynamic URLs, or low-level control.
-- Simple GET returning a string
$response = call rest service get 'https://api.example.com/data' (
Headers: ('Accept': 'application/json'),
Timeout: 30,
) returns string;
-- GET with URL template parameters
$response = call rest service get 'https://api.example.com/users/{1}' with (
{1} = toString($UserId)
) (
Headers: ('Accept': 'application/json'),
) returns string;
-- POST with body
$response = call rest service post 'https://api.example.com/items' (
Headers: ('Content-Type': 'application/json'),
Body: template '{"name": "test"}',
) returns string;
-- With basic auth
$response = call rest service get 'https://api.example.com/secure' (
Authentication: basic (Username: 'username', Password: 'password'),
) returns string;
-- With import mapping (JSON → entity)
$item = call rest service get 'https://api.example.com/item/1' (
Headers: ('Accept': 'application/json'),
) returns mapping Module.IMM_Item as Module.Item;
-- Fire and forget
call rest service delete 'https://api.example.com/item/1'
returns nothing;
-- Error handling
$response = call rest service get 'https://api.example.com/data'
returns string
on error continue;
Data Transformers (JSLT — Mendix 11.9+)
Transform complex JSON responses into simpler structures before import mapping.
-- Define the transformer
create data transformer Module.SimplifyWeather
source json '{"latitude": 52.52, "current": {"temperature_2m": 12.8, "wind_speed_10m": 18.3}}'
{
jslt $$
{
"temp": .current.temperature_2m,
"wind": .current.wind_speed_10m,
"lat": .latitude
}
$$;
};
-- Use in a microflow
$SimplifiedJson = transform $RawJson with Module.SimplifyWeather;
Single-line JSLT: jslt '{ "temp": .current.temperature_2m }';
Multi-line JSLT: jslt $$ { ... } $$; (dollar-quoting, same as Java actions)
list data transformers [in module];
describe data transformer Module.Name;
drop data transformer Module.Name;
JSON Structures & Mappings
See json-structures-and-mappings for full reference. Quick summary:
mdl 1;
-- JSON structure from snippet
create json structure Module.JSON_Weather
sample '{"temp": 12.8, "wind": 18.3, "lat": 52.52}';
-- Non-persistent entity
create non-persistent entity Module.WeatherInfo (
Temperature: decimal,
WindSpeed: decimal,
Latitude: decimal
);
-- Import mapping (JSON → entity)
create import mapping Module.IMM_Weather
with json structure Module.JSON_Weather
{
create Module.WeatherInfo {
Temperature = temp,
WindSpeed = wind,
Latitude = lat
}
};
Use them in a microflow:
$Weather = import from mapping Module.IMM_Weather($JsonString);
$JsonOutput = export to mapping Module.EMM_Weather($WeatherEntity);
Complete Pipeline Example
Full example: call weather API → transform → import → show on page.
mdl 1;
-- 1. Entity
create non-persistent entity Module.CurrentWeather (
Temperature: decimal,
WindSpeed: decimal,
Latitude: decimal,
ObservationTime: datetime
);
-- 2. Data Transformer (simplify API response)
create data transformer Module.SimplifyWeather
source json '{"latitude":52.52,"current":{"time":"2024-01-15T14:00","temperature_2m":12.8,"wind_speed_10m":18.3}}'
{
jslt $$
{
"temperature": .current.temperature_2m,
"windSpeed": .current.wind_speed_10m,
"latitude": .latitude,
"observationTime": .current.time
}
$$;
};
-- 3. JSON Structure + Import Mapping (for transformed output)
create json structure Module.JSON_Weather
sample '{"temperature":12.8,"windSpeed":18.3,"latitude":52.52,"observationTime":"2024-01-15T14:00"}';
create import mapping Module.IMM_Weather
with json structure Module.JSON_Weather
{
create Module.CurrentWeather {
Temperature = temperature,
WindSpeed = windSpeed,
Latitude = latitude,
ObservationTime = observationTime
}
};
-- 4. REST Client
create consumed rest service Module.WeatherAPI (
BaseUrl: 'https://api.open-meteo.com/v1',
authentication: none
)
{
operation GetCurrent (
method: get,
path: '/forecast',
query: ($latitude: decimal, $longitude: decimal, $current: string),
headers: ('Accept': 'application/json'),
response: json as $Result
)
};
-- 5. Microflow (REST Client → Transform → Import)
create microflow Module.ACT_GetWeather ()
returns Module.CurrentWeather as $Weather
begin
send rest request Module.WeatherAPI.GetCurrent;
declare $RawJson string = $latestHttpResponse/content;
$SimplifiedJson = transform $RawJson with Module.SimplifyWeather;
$Weather = import from mapping Module.IMM_Weather($SimplifiedJson);
return $Weather;
end;
Published REST: Authentication Parameters
The other direction — an API the app publishes — has one REST-specific
trap: a custom-authentication microflow that takes a token. Every parameter
besides System.HttpRequest / System.HttpResponse must be bound to a query
or header value of the service, or mx check reports CE0336 ("not present in
the custom authentication parameter list"). Bind them after the microflow:
mdl 1;
create or modify published rest service Shop.Api (
Path: 'rest/shop/v1', Version: '1.0.0', ServiceName: 'Shop',
Authentication: (
basic,
microflow Shop.Authenticate (
$Token: header 'X-Api-Token', -- HTTP name differs from $Token
$Tenant: query, -- ?Tenant=
$Lang: query 'lang' description 'UI language, defaults to en'
)
)
) { resource 'orders' { get '' microflow Shop.GetOrders; } };
- The list is optional: an unlisted parameter is a
queryparameter under its own name, somicroflow Shop.Authenticatealone is enough for?Token=. - Never write a type — it is the microflow parameter's. Only
queryandheader; an unknown$Param, the request/response, or a duplicate is MDL-REST04. describeprints the list only when it differs from that default.mxcli syntax rest.publishedhas the rest of the statement.
Mendix Validation Rules
| Rule | Error | Fix |
|---|---|---|
| Every operation MUST have an Accept header | CE7062 | Auto-added by serializer if missing |
| POST/PUT/PATCH MUST have a body | CE7064 | Auto-added by serializer (empty JSON body) |
| Template placeholders must match parameters | CE7056 | {name} requires a parameter named name |
| No custom error handling on SEND REST REQUEST | CE6035 | Always uses abort-on-error semantics |
| Data Transformer requires 11.9+ | version check | checkFeature("integration", "data_transformer", ...) |
BSON Types Reference
| MDL Concept | BSON Type |
|---|---|
| REST client document | rest$ConsumedRestService |
| Operation | rest$RestOperation |
| GET/DELETE method | rest$RestOperationMethodWithoutBody |
| POST/PUT/PATCH method | rest$RestOperationMethodWithBody |
| JSON body | rest$JsonBody |
| Template body | rest$StringBody |
| Export mapping body | rest$ImplicitMappingBody |
| No response | rest$NoResponseHandling |
| Import mapping response | rest$ImplicitMappingResponseHandling |
| Header | rest$HeaderWithValueTemplate |
| Path parameter | rest$OperationParameter |
| Query parameter | rest$QueryParameter |
| Data transformer | DataTransformers$DataTransformer |
| JSLT step | DataTransformers$JsltAction |
| Transform action | microflows$TransformJsonAction |
| Send request action | microflows$RestOperationCallAction |
| Inline HTTP action | microflows$RestCallAction |