# The API Gateway Handbook - Code Snippets In the API Gateway book: https://www.membrane-api.io/api-gateway-ebook.html contains many configuration examples, API requests, and URLs. In the PDF version, these snippets can be difficult to copy accurately. To make things easier, this document collects the most relevant examples so you can copy and paste them directly into Membrane configurations or use them in your terminal. # 24 Membrane API Gateway ## 24.1 Standalone Java Installation curl http://localhost:2000/fact curl http://localhost:2000/shop/v2 ## 24.2 Docker Installation docker run -it -p 2000:2000 predic8/membrane ### On macOS/Linux: docker run -it -p 2000:2000 -v "$(pwd)/conf/apis.yaml:/opt/membrane/conf/apis.yaml" predic8/membrane ### On Windows (PowerShell or CMD): docker run -it -p 2000:2000 -v ${PWD}\conf\apis.yaml:/opt/membrane/conf/apis.yaml predic8/membrane # 25 API Configuration # yaml-language-server: $schema=https://www.membrane-api.io/v7.1.1.json api: port: 2000 target: url: https://api.predic8.de # 26 Routing Traffic api: port: 2000 method: GET path: uri: /shop/v2 target: url: https://api.predic8.de curl http://localhost:2000/shop/v2/ ## 26.1 Order of API Matching api: name: API 1 port: 2000 method: GET flow: - log: message: "Method: ${method}" target: url: https://api.predic8.de --- api: name: API 2 port: 2000 method: POST flow: - log: message: "Method: ${method}" target: url: https://api.predic8.de ### Case 1: Sending a GET request curl http://localhost:2000/shop/v2/products ### Case 2: Sending a POST request POST /shop/v2/products Host: localhost:2000 Content-Type: application/json { "name": "Biscuits", "price": 1.99 } curl -X POST -H 'Content-Type: application/json' -d '{"name": "Biscuits", "price": 1.99}' http://localhost:2000/shop/v2/products ### Case 3: Sending a request with an unsupported method curl -X PUT http://localhost:2000 ## Content-Based Routing Pattern api: port: 2000 test: json['name'] == 'Lolly' flow: - static: src: "Sweets this way!" - return: status: 200 POST /shop/v2/products Host: localhost:2000 Content-Type: application/json { "name": "Lolly" } ## Temporarily Blocking Access api: port: 2000 path: uri: /products flow: - request: - static: contentType: application/json src: | { "message": "Temporarily unavailable" } - return: status: 503 ## 26.4 URI Templates api: port: 2000 path: uri: /customers/{cid}/orders/{oid} flow: - log: message: Order ${pathParam.oid} for ${pathParam.cid} - return: status: 200 --- api: port: 2000 path: uri: /customers/{cid} flow: - log: message: 'Customer: ${pathParam.cid}' - return: status: 200 curl http://localhost:2000/customers/88 curl http://localhost:2000/customers/88/orders/3 ## 27.1 Exchange Properties api: port: 2000 flow: - setProperty: name: color value: RED - setHeader: name: X-Size value: XL - log: message: | Header ${header['X-Size']} Prop ${property.color} target: url: http://localhost:2001 --- api: port: 2001 flow: - request: - log: message: | Header ${header['X-Size']} Prop ${property.color} - return: status: 200 # 28 OpenAPI ## 28.1 Using OpenAPI for Gateway Configuration api: port: 2000 openapi: - location: openapi/dlp-v1.0.0.oas.yml http://localhost:2000/api-docs ### Loading Multiple APIs api: port: 2000 openapi: - location: openapi/dlp-v1.0.0.oas.yml - location: openapi/fruitshop-v2-2-0.oas.yml ## 28.2 Rewriting of OpenAPI Addresses openapi: - location: openapi/fruitshop-v2-2-0.oas.yml rewrite: host: api.predic8.de protocol: https port: 443 basePath: /store ## 28.3 OpenAPI Message Validation api: port: 2000 openapi: - location: openapi/fruitshop-v2-2-0.oas.yml validateRequests: true ## 28.4 APIOps with OpenAPI FROM predic8/membrane USER root RUN apt-get update && \ apt-get install -y wget && \ wget "https://github.com/predic8/rfq-api/releases/latest/download/rfq-api-v1.oas.yml" \ -O /opt/membrane/conf/openapi/rfq.oas.yml USER membrane COPY apis.yaml /opt/membrane/conf EXPOSE 2000 ENTRYPOINT ["/opt/membrane/membrane.sh"] docker build -t membrane:1 . docker run -it -p 2000:2000 membrane:1 http://localhost:2000/api-docs # 29 Transformation and Message Manipulation ## 29.1 Manipulating HTTP Headers api: port: 2000 flow: - response: - setHeader: name: X-Foo value: '42' - return: status: 200 curl -i http://localhost:2000 ## 29.2 Passing HTTP Headers to Backends ### Gateway API: Adding a Token api: port: 2000 flow: - request: - setHeader: name: X-Token value: abc123 target: url: http://localhost:3000 ### Backend: Logging the Token api: port: 3000 flow: - request: - log: message: 'Got: ${header["X-Token"]}' - return: status: 200 ### Testing the Configuration curl http://localhost:2000 ## 29.3 Computing Header and Property Values api: port: 2000 flow: - request: - setHeader: name: X-Prod-Env value: ${header['host'] matches 'prod.*'} language: spel - setHeader: name: X-Date value: ${java.time.LocalDate.now()} language: groovy - setHeader: name: X-Address value: ${$.address.zip} ${$.address.city} language: jsonpath - log: message: "Headers:\n ${header}" - return: status: 200 POST http://localhost:2000 Content-Type: application/json Host: prod.example.com { "address": { "city": "Boston", "zip": "02108" } } ## 29.4 Removing HTTP Headers api: port: 2000 flow: - response: - headerFilter: - include: Content.* - include: last-modified - exclude: .* target: url: https://www.wikipedia.org curl -sS -D - https://www.wikipedia.org -o /dev/null curl -sS -D - http://localhost:2000 -o /dev/null ## 29.6 Make It Nice api: port: 2000 flow: - request: - beautifier: {} - return: status: 200 POST http://localhost:2000 Content-Type: application/json { "a":{ "b":{"c":7}}} ## 29.7 Transformation Between JSON and XML ### Generic JSON to XML Transformation api: port: 2000 flow: - response: - json2Xml: root: fruit target: url: https://api.predic8.de curl http://localhost:2000/shop/v2/products/7 ### XML2JSON Transformation api: port: 2000 flow: - request: - xml2Json: {} target: url: https://api.predic8.de POST http://localhost:2000/shop/v2/products Content-Type: text/xml Citron 0.39 ## 29.8 Rendering Dynamic Content api: port: 2000 flow: - request: - setBody: language: xpath contentType: application/json value: | { "name": ${/add/name}, "price": ${/add/price} } target: url: https://api.predic8.de ### Transforming a GET into a POST api: port: 2000 flow: - request: - template: contentType: application/json src: | { "name": ${param.name[0]}, "price": ${param.price[0].toFloat()} } target: method: POST url: https://api.predic8.de/shop/v2/products ### Transforming a POST into a GET api: port: 2000 target: method: GET url: https://api.predic8.de/shop/v2/products/${$.id} language: jsonpath Host: localhost:2000 Content-Type: application/json { "id": 13 } ### Processing Lists and Map Structures api: port: 2000 flow: - request: - template: contentType: application/json pretty: true src: | { <% header.eachWithIndex { e, i -> %> <% if (i > 0) { %>,<% } %> <%= e.key %>: <%= e.value %> <% } %> } - return: status: 200 # 30 Control Flow ## 30.1 Conditions api: port: 2000 flow: - request: - if: test: header['X-Api-Key'] != 'sesame' flow: - return: status: 401 - return: status: 200 curl -H "X-Api-Key: sesame" localhost:2000 -v ### Choose and Case api: port: 2000 flow: - response: - choose: - case: test: statusCode == 401 flow: - static: contentType: application/problem+json src: | { "type": "/problem/auth", "title": "Authentication Error" } - case: test: statusCode >= 400 and statusCode < 500 flow: - static: contentType: application/problem+json src: | { "type": "/problem/client", "title": "Client Error" } - otherwise: - log: message: No error target: url: http://localhost:3000 # 31 API Orchestration ## 31.1 Aggregating Backend APIs https://openlibrary.org/books/OL29474405M.json https://openlibrary.org/authors/OL272947A.json ### Composing Two Backend Calls api: port: 2000 path: uri: /books/{id} flow: - request: - call: url: https://openlibrary.org/books/${pathParam.id}.json - setProperty: name: title value: ${$.title} language: jsonpath - call: url: https://openlibrary.org${$.authors[0].key}.json language: jsonpath - template: contentType: application/json src: | { "title": ${property.title}, "author": ${jsonPath('$.name')} } - return: status: 200 curl http://localhost:2000/books/OL29474405M curl http://localhost:2000/books/OL26333978M ## 31.2 Authentication for Backend Access ### 1. Target API (Backend Simulation at port 3001) api: port: 3001 name: Target API flow: - request: - log: message: "Session: ${cookie.SESSION}" - if: test: cookie.SESSION != 'akj34' flow: - return: status: 401 - static: src: Success! - return: status: 200 ### 2. Authentication Service (port 3000) api: port: 3000 path: uri: /login flow: - response: - setCookies: - name: SESSION value: akj34 - log: message: Login successful. Issue cookie. - return: status: 200 ### 3. Orchestration API (port 2000) api: port: 2000 flow: - request: - log: message: Authenticating via login API - call: url: http://localhost:3000/login - log: message: "Got Cookie: ${header['Set-Cookie']}" - setHeader: name: Cookie value: ${header['Set-Cookie'].split(';')[0]} - headerFilter: - exclude: Set-Cookie - log: message: "Forwarding Cookie: ${header['Cookie']}" target: url: http://localhost:3001 ## 31.3 Processing RESTful List Resources ### Implementation api: port: 2000 flow: - request: - call: url: https://api.predic8.de/shop/v2/products?limit=7 - for: in: $.products language: jsonpath flow: - log: message: 'Getting price for: ${it["name"]}' - call: url: https://api.predic8.de${it['self_link']} - groovy: src: | property.res = property.res ?: [] property.res.add( [ "name": it['name'], "price": jsonPath('$.price') ]) - template: contentType: application/json pretty: true src: | { "products": <%= property.res %> } - return: status: 200 # 32 Secure Data in Transit with TLS ## 32.1 Reaching Backends over TLS ### TLS to a Backend With Public Certificate api: port: 2000 target: url: https://api.predic8.de ## 32.2 Termination of TLS Connections openssl x509 -in membrane.pem -text ### Configuring TLS Termination api: port: 443 ssl: key: private: location: membrane-key.pem certificates: - location: membrane.pem flow: - request: - log: {} target: url: https://api.predic8.de ### Testing the TLS Setup curl -v -k https://localhost:443 ### Forwarding TLS Connections Without Decryption sslProxy: port: 443 host: secret.example.com target: host: secret.example.com port: 443 ## 32.3 Debugging TLS Connectivity api: port: 443 ssl: showSSLExceptions: true # 33 Access Control Lists global: - accessControl: - deny: 192.168.3.8 - deny: 192.168.10.0/24 - allow: 192.168.0.0/16 - allow: '.+\.predic8\.de$' api: port: 9000 flow: - accessControl: - allow: 127.0.0.0/8 - allow: localhost - adminConsole: {} # 34 Content Protection ## 34.1 JSON Protection api: port: 2000 flow: - jsonProtection: {} - return: status: 200 POST http://localhost:2000 Content-Type: application/json { "price": 10, "price": -1 } ## 34.2 XML Protection global: - xmlProtection: {} POST / Host: localhost:2000 Content-Type: text/xml ]> Hello ## 34.3 GraphQL Protection global: - graphQLProtection: maxRecursion: 1 maxMutations: 10 maxDepth: 3 disallow: - mutation: updateCategory - introspection: {} # 35 Basic Authentication ### Setting up Basic Authentication api: port: 2000 flow: - basicAuthentication: users: - username: alice password: "qwertz" - username: carol password: "$6$k3jia$NlCL.JTXitlwZ4i672In7c8M..." - static: src: You're in! - return: status: 200 curl -u alice:qwertz http://localhost:2000 curl -u carol:abc123 http://localhost:2000 openssl passwd -6 -salt k3jia "yourPassword" ### How to Generate a .htpasswd File htpasswd -c .htpasswd alice htpasswd .htpasswd bob # 36 API Keys global: - apiKey: stores: - simple: - value: aed8bcc4-7c83-44d5-8789-21e4024ac873 - value: 61e5a2d8-21aa-4d0b-bdae-76e60ee52803 extractors: - header: X-Api-Key GET http://localhost:2000 X-Api-Key: 61e5a2d8-21aa-4d0b-bdae-76e60ee52803 ### 1. Using HTTP header GET / HTTP/1.1 Host: localhost:2000 X-API-KEY: aed8bcc4-7c83-44d5-8789-21e4024ac873 ### 2. Using query string GET /products?api-key=aed8bcc4-7c83-44d5-8789-21e4024ac873 Host: localhost:2000 ### 3. Using JSON payload POST / HTTP/1.1 Host: localhost:2000 Content-Type: application/json { "api-key": "61e5a2d8-21aa-4d0b-bdae-76e60ee52803" } ## 36.1 Storing API Keys ### Storing API Keys in a File 3141 key_321_abc: admin 7a26cae9-ed29-40b3-bc99-5b1914bb8498: read, write components: api-key-store: apiKeyFileStore: location: keys.txt ### Storing API Keys in a Database components: dataSource: bean: class: org.apache.commons.dbcp2.BasicDataSource properties: - name: driverClassName value: org.postgresql.Driver - name: url value: jdbc:postgresql://localhost:5432/postgres - name: username value: postgres - name: password value: secret --- global: - apiKey: stores: - databaseApiKeyStore: datasource: '#/components/dataSource' keyTable: key scopeTable: scope ### MongoDB API Key Store api: port: 2000 flow: - apiKey: stores: - mongoDBApiKeyStore: connection: mongodb://localhost:27017/ database: apiKeyDB collection: apikey target: url: https://api.predic8.de mongosh --eval "use('apiKeyDB'); db.apikey.insertMany([ { id: '345%FSe3', scopes: ['read', 'write'] }, { id: '3c7f6c34', scopes: ['read'] }, { id: '343265FA', scopes: ['read', 'admin'] }, { id: 'flower2025', scopes: ['read', 'write'] }]);" ## 36.2 Role-based Access Control (RBAC) api: port: 2000 flow: - apiKey: stores: - apiKeyFileStore: location: api-keys.txt extractors: - header: X-Api-Key - request: - setHeader: name: X-Scopes value: ${scopes()} target: url: http://localhost:3000 api: port: 3000 path: uri: /admin flow: - request: - if: test: not header['X-Scopes'].contains('admin') flow: - static: src: Only admins! - return: status: 403 - static: src: You're in! - return: status: 200 # 37 JSON Web Tokens ## 37.1 Issuing JWTs ### Step 1: Generate a Private Key On Windows: membrane.cmd generate-jwk -o demo.jwk On Linux: membrane.sh generate-jwk -o demo.jwk ### Step 2: Configure Membrane to Issue Tokens api: port: 2000 name: Token Server path: uri: /token flow: - request: - template: contentType: application/json src: | { "sub": "user@predic8.de", "aud": "order" } - jwtSign: jwk: location: demo.jwk - return: {} curl http://localhost:2000/token ## 37.2 Protecting the Token Generation Process ### Requiring an API Key Before Issuing a Token api: port: 2000 name: Token Server path: uri: /token flow: - apiKey: stores: - apiKeyFileStore: location: demo-keys.txt extractors: - header: X-Api-Key - request: - template: contentType: application/json src: | { "sub": "api-user", "aud": "order", "scope": ${scopes()} } - jwtSign: jwk: location: demo.jwk - return: status: 200 key_321_abc: admin 3141: finance 123456789: finance, accounting 7a26cae9-ed29-40b3-bc99-5b1914bb8498: read, write ## 37.3 Verifying JWTs ### Step 1: Set Up a Demo API api: port: 3000 name: Demo API flow: - template: src: | { "content": "Protected content!" } - return: status: 200 ### Step 2: Protect the API with Membrane api: port: 4000 name: Secured Access to Demo API flow: - jwtAuth: expectedAud: order jwks: - location: demo.jwk target: url: http://localhost:3000 curl -H "Authorization: Bearer <" localhost:4000 # 38 OAuth2 and OpenID Connect ## 38.1 Token Verification https://login.microsoftonline.com/common/discovery/keys # 39 Legacy Integration of SOAP Web Services ## 39.1 Sample Web Services Mock api: port: 2000 path: uri: /city-service flow: - sampleSoapService: {} http://localhost:2000/city-service?wsdl POST /city-service Host: localhost:2000 Content-Type: text/xml Bonn ## 39.2 Mocking a Web Service api: port: 2000 flow: - response: - static: pretty: true contentType: text/xml src: | England 8980000 - return: status: 200 ### Using the SoapBody Template api: port: 2000 flow: - response: - soapBody: version: '1.2' pretty: true src: | England 8980000 - return: status: 200 HTTP/1.1 200 OK Content-Type: application/soap+xml England 8980000 ## 39.3 Exposing SOAP Web Services as REST APIs ### REST GET to SOAP api: port: 2000 method: GET path: uri: /cities/{city} flow: - request: - soapBody: src: | ${pathParam.city} - setHeader: name: SOAPAction value: https://predic8.de/cities/get - response: - setBody: language: xpath contentType: application/json value: | { "country": ${//country}, "population": ${//population} } target: method: POST url: https://www.predic8.de/city-service ### Transforming JSON Lists into XML { "fruits": [ { "name": "Apricot","price": 3.87 }, { "name": "Date", "price": 2.35 } ] } - soapBody: pretty: true src: | <% jsonPath('$.fruits').each { it -> %> ${it.name} ${it.price} <% } %> ### Transforming XML Lists to JSON Apricot 3.87 Date 2.35 components: ns: xmlConfig: namespaces: - prefix: f uri: https://predic8.de/fruits - template: pretty: true contentType: application/json src: | <% def fruits = xpath('//f:fruit') def total = fruits.getLength() %> { "fruits": [ <% for (int i = 0; i < total; i++) { def fruit = fruits.item(i) %> { "name": <%=xpath('string(./f:name)', fruit)%>, "price": <%=xpath('number(./f:price)',fruit)%> }<%= (i < total - 1) ? "," : "" %> <% } %> ] } ### Using Groovy for Message Transformation - groovy: src: | import groovy.xml.XmlSlurper import groovy.json.JsonOutput def xml = body.toString() def root = new XmlSlurper(false, true).parseText(xml) root.declareNamespace( s11: "http://schemas.xmlsoap.org/soap/envelope/", f: "https://predic8.de/fruits" ) def fruits = root.'s11:Body' .'f:getFruitsResponse' .'f:fruit'.collect { n -> [ name : n.'f:name'.text(), price: new BigDecimal(n.'f:price'.text()) ] } def payload = JsonOutput.toJson([fruits: fruits]) message.setBodyContent(payload.getBytes()) api: port: 2000 path: uri: /products flow: - request: - groovy: location: json-to-soap-request.groovy - response: - if: test: //*[local-name()='Fault'] language: xpath flow: - groovy: location: soap-fault-to-json.groovy else: - groovy: location: soap-response-to-json.groovy target: url: http://localhost:2000/product-service ### Routing SOAP Requests api: port: 2000 method: POST path: uri: /products components: ns: xmlConfig: namespaces: - prefix: ps uri: https://predic8.de/products --- api: port: 2000 test: //ps:getProducts language: xpath target: url: http://localhost:3000 --- api: port: 2000 test: //ps:createProduct language: xpath target: url: http://localhost:3001 ### SOAPAction Routing api: port: 2000 test: header.SOAPAction == 'https://www.predic8.de/get-city' target: url: http://localhost:3000 --- api: port: 2000 test: header.SOAPAction == 'https://www.predic8.de/create-city' target: url: http://localhost:3001 ## 39.5 Best Practices ### Prompt: For a SOAP to REST Transformation Create a Membrane API Gateway configuration that exposes the REST endpoint POST /shop/v2/products as SOAP Web Service. Map SOAP version 1.1 request bodies to JSON and map JSON responses back to SOAP, including status codes and Content-Type headers. Create a lean solution. Do not include security, logging, health checks, or other unneeded features. Deliverables: - A Membrane YAML configuration (v7) implementing the mapping. Request and response templates in Groovy for the mapping. - A WSDL with the SOAP operation, including XSD, messages, portType, binding and service. - Example curl commands and SOAP requests for testing. - Document the mapping in comments in the YAML and WSDL. - Error handling: Map REST status code to SOAP Faults. - Guide explaining how to set up the solution Assumptions: - REST request contains name and price. - If any of these assumptions are incorrect, infer the most reasonable mapping. # 40 Proxying SOAP ### A Proxy for SOAP Services soapProxy: port: 2000 wsdl: https://www.predic8.de/city-service?wsdl http://localhost:2000/city-service?wsdl ### Rewriting of WSDL URLs http://localhost:2000/city-service?wsdl soapProxy: path: uri: /my-service port: 2000 wsdl: https://www.predic8.de/city-service?wsdl flow: - wsdlRewriter: host: my.host.example.com protocol: https port: 443 path: /my-service ### WSDL Validation soapProxy: port: 2000 wsdl: https://www.predic8.de/city-service?wsdl flow: - validator: {} ### How a soapProxy works soapProxy: port: 2000 wsdl: https://www.predic8.de/city-service?wsdl flow: - log: message: "Path: ${path}" - rateLimiter: requestLimit: 1000 requestLimitDuration: PT1H # 41 Operation ## 41.1 Admin Console ### Activating the Console api: port: 9000 flow: - adminConsole: {} ### Securing the Console api: port: 9000 flow: - basicAuthentication: users: - username: admin password: ba9b-fcd63 - adminConsole: readOnly: true ## 41.2 Monitoring with Prometheus and Grafana ### Prometheus api: port: 2000 path: uri: /metrics flow: - prometheus: {} curl localhost:2000/metrics ## 41.4 API Tracing with OpenTelemetry ### Setting up OpenTelemetry global: - openTelemetry: sampleRate: 1.0 otlpExporter: host: localhost transport: grpc ## 41.5 Message Logging ### Logging What Matters - log: message: | Got: ${body} Header: ${header} Trace: ${header['Trace-Id']} - log: message: "Fruit: ${$.name}" language: jsonpath - log: message: "Fruit: ${//name}" language: xpath ## 41.6 Message Stores ### In-Memory Store components: store: limitedMemoryExchangeStore: maxSize: 222222222 ### File Message Store components: my-store: fileExchangeStore: dir: exchanges maxDays: 7 ### MongoDB Message Store components: my-store: mongoDBExchangeStore: connection: mongodb://localhost:27017/ database: membrane collection: exchanges