--- name: rest-client description: "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." --- # 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](../mock-rest-apis/SKILL.md) 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.0 spec (JSON or YAML), generate the REST client in one command: ```sql 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](../mock-rest-apis/SKILL.md)). **Preview without writing:** ```sql 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. --- ## 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 ```sql 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). ```text 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 ```sql -- No authentication authentication: none -- Basic auth authentication: basic (username: 'user', password: 'secret') ``` ### Body Types ```sql -- 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: ```sql 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 ```sql -- Simple types (variable binding is at the call site, not stored on the operation) 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`: ```sql 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: ```sql 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 ```sql 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 ```sql 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 ``` --- ## 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. ```sql -- 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. ```sql -- 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) ```sql list data transformers [in module]; describe data transformer Module.Name; drop data transformer Module.Name; ``` --- ## JSON Structures & Mappings See [json-structures-and-mappings](../json-structures-and-mappings/SKILL.md) for full reference. Quick summary: ```sql 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: ```sql $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. ```sql 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; ``` --- ## 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` |