diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ec7931d614a55e9107df68de039b151b1b0a65dd..667c03eba3d066f8b58643e62ee6bfb222a91014 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -22,7 +22,7 @@ build:
   image: openjdk:11
   stage: build
   script:
-    - ./gradlew --console=plain --no-daemon assemble jar
+    - ./gradlew --console=plain --no-daemon assemble testClasses jar
   artifacts:
     paths:
       - "ragconnect.base/build/libs/ragconnect-*.jar"
diff --git a/pages/docs/compiler.md b/pages/docs/compiler.md
index ccff6d6c05f7544ff75caeda4f19a6f87c765ad9..93b0bc0f48ec7e1e6083fe97236c5a8701057135 100644
--- a/pages/docs/compiler.md
+++ b/pages/docs/compiler.md
@@ -5,21 +5,21 @@ Additional options are as follows.
 
 ## Table with available options
 
-| Name | Required (Default) | Description |
-|---|---|---|
-| `--rootNode` | Yes | Root node in the base grammar. |
-| `--protocols` | No (`mqtt`) | Protocols to enable, currently available: `java` (experimental), `mqtt`, `rest`. |
-| `--printYaml` | No (false) | Print out YAML instead of generating files. |
-| `--verbose` | No (false) | Print more messages while compiling. |
-| `--logReads` | No (false) | Enable logging for every received message. |
-| `--logWrites` | No (false) | Enable logging for every sent message. |
-| `--logIncremental` | No (false) | Enable logging for observer in incremental dependency tracking. |
-| `--logTarget` | No (`console`) | Logging target to use, currently available: `console, slf4j`. |
-| `--experimental-jastadd-329` | No (false) | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking](using.md#dependency-tracking-automatically-derived). |
-| `--incremental` | No (false) | Enables incremental dependency tracking (if `tracing` is also set appropriately). |
-| `--tracing[=flush]` | No (false) | Enables incremental dependency tracking (if `incremental` is also set appropriately). |
-| `--version` | No (false) | Print version info and exit (reused JastAdd option) |
-| `--o` | No (`.`) | Output directory (reused JastAdd option) |
+| Name                         | Required (Default) | Description                                                                                                                                                                           |
+|------------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `--rootNode`                 | Yes                | Root node in the base grammar.                                                                                                                                                        |
+| `--protocols`                | No (`mqtt`)        | Protocols to enable, one of `java` (experimental), `mqtt`, `rest`, see [Handlers][handlers] for details.                                                                              |
+| `--printYaml`                | No (false)         | Print out YAML instead of generating files.                                                                                                                                           |
+| `--verbose`                  | No (false)         | Print more messages while compiling.                                                                                                                                                  |
+| `--logReads`                 | No (false)         | Enable logging for every received message.                                                                                                                                            |
+| `--logWrites`                | No (false)         | Enable logging for every sent message.                                                                                                                                                |
+| `--logIncremental`           | No (false)         | Enable logging for observer in incremental dependency tracking.                                                                                                                       |
+| `--logTarget`                | No (`console`)     | Logging target to use, currently available: `console, slf4j`.                                                                                                                         |
+| `--experimental-jastadd-329` | No (false)         | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking][automatic-dependency-tracking]. |
+| `--incremental`              | No (false)         | Enables incremental dependency tracking (if `tracing` is also set appropriately).                                                                                                     |
+| `--tracing[=flush]`          | No (false)         | Enables incremental dependency tracking (if `incremental` is also set appropriately).                                                                                                 |
+| `--version`                  | No (false)         | Print version info and exit (reused JastAdd option)                                                                                                                                   |
+| `--o`                        | No (`.`)           | Output directory (reused JastAdd option)                                                                                                                                              |
 
 All files to be processed have to be passed as arguments.
 Their type is deduced by the file extension (`ast` and `relast` for input grammars, `connect` and `ragconnect` for RagConnect definitions file).
@@ -29,44 +29,6 @@ Their type is deduced by the file extension (`ast` and `relast` for input gramma
 Using RagConnect itself does not introduce dependencies.
 However, depending on the selected protocols and/or used features, additional dependencies are required when using the generated code.
 
-### Communication protocol characteristics
-
-#### Java
-
-- Protocol identifier: `java`
-- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>`
-    - the value for host and port are always ignored, but are necessary to form a legal URI
-- No required runtime dependencies
-- Additional remarks:
-    - First leading slash not included in topic.
-    - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions.
-
-#### MQTT
-
-- Protocol identifier: `mqtt`
-- URI scheme: `mqtt://<broker-host>[:port]/<topic>`
-- Default port: 1883
-- Type for mapping definitions: `byte[]`
-- Required runtime dependencies:
-    - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'`
-- Additional remarks:
-    - First leading slash not included in topic.
-    - Mqtt is selected by default, so this dependency therefore is required "by default".
-    - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well.
-
-#### REST
-
-- Protocol identifier: `rest`
-- URI scheme: `rest://localhost[:port]/<path>`
-- Default port: 4567
-- Type for mapping definitions: `String`
-- Required runtime dependencies:
-    - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'`
-- Additional remarks:
-    - Host is always `localhost`.
-    - Might work with newer versions of `com.sparkjava.spark-core` as well.
-    - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j].
-
 ### Used features
 
 #### Automatic dependency tracking
@@ -116,4 +78,5 @@ However, depending on the selected protocols and/or used features, additional de
     - Additionally, a slf4j binding is required, see [the slf4j user manual][slf4j]
 
 [jastadd-issue-329]: https://bitbucket.org/jastadd/jastadd2/issues/329/add-event-for-completion-of-flush
-[slf4j]: https://www.slf4j.org/manual.html
+[automatic-dependency-tracking]: using.md#dependency-tracking-automatically-derived
+[handlers]: handlers.md
diff --git a/pages/docs/extending.md b/pages/docs/extending.md
index 14fa8c8dbb525900f26c0ccbf9f7b45ac17544e5..4ed4ce1285b402a747f69f59580310a10c037a92 100644
--- a/pages/docs/extending.md
+++ b/pages/docs/extending.md
@@ -5,9 +5,9 @@ To add a new communication protocol, the following locations have to be changed
 ### Within `ragconnect.base/src/main/resources`
 
 {% raw %}
-- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. 
+- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. A handler must have a constructor accepting a single String parameter, and must have a `close()` method cleaning up any held resources. 
 - In `handler.mustache`, add further methods if needed for handler usage in the application code (similar to `{{rootNodeName}}.{{SetupWaitUntilReadyMethodName}}` for `mqtt`)
-- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions. If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `rest`.
+- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions (that are four distinct locations). If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `restClient`.
 {% endraw %}
 
 ### Within `ragconnect.base/src/main/jastadd`
@@ -18,7 +18,7 @@ In `Handlers.jrag`: Add a new attribute `RagConnect.abcHandler()` returning the
 
 In `Compiler.java`:
 - Add a new choice for `--protocols` similar to the existing ones
-- Add a newly constructed handler in `setConfiguration`  with the needed fields (definition file name within `resources` directory, commonly `ABCHandler.jadd`; class name of the handler; unique name for the protocol; whether the handler is used, i.e., if it was given in `--protocols`)
+- Add a newly constructed handler in `setConfiguration`  with the needed fields (class name of the handler; unique name for the protocol (must be a valid Java identifier); whether the handler is used, i.e., if it was given in `--protocols`)
 
 Furthermore, new test cases are appreciated, see [below](#writing-tests).
 
diff --git a/pages/docs/handlers.md b/pages/docs/handlers.md
new file mode 100644
index 0000000000000000000000000000000000000000..f126ea126b73c7568f9cf753eb712d6c2f80ed8e
--- /dev/null
+++ b/pages/docs/handlers.md
@@ -0,0 +1,70 @@
+# Communication Protocol Characteristics (Handlers)
+
+## Java (experimental)
+
+Uses Java methods to supply values (receive) and for callbacks (send).
+
+- Protocol identifier: `java`
+- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>`
+    - the value for host and port are always ignored, but are necessary to form a legal URI
+- No required runtime dependencies
+- Receive behaviour: Use the generated method `ragconnectJavaPush` to pass a value to the receiving port.
+- Send behaviour: When the value to be sent changes, previously registered callbacks are invoked.
+- Additional remarks:
+    - First leading slash not included in topic.
+    - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions.
+
+
+## MQTT
+
+Use an MQTT broker to receive and send messages.
+
+- Protocol identifier: `mqtt`
+- URI scheme: `mqtt://<broker-host>[:port]/<topic>`
+- Default port: 1883
+- Type for mapping definitions: `byte[]`
+- Required runtime dependencies:
+    - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'`
+- Receive behaviour: Upon connection, instruct the MQTT broker to listen for messages on some topic and pass the value of those messages to the receiving port.
+- Send behaviour: When the value to be sent changes, publish a message to the topic specified upon connection.
+- Additional remarks:
+    - First leading slash not included in topic.
+    - Mqtt is selected by default, so this dependency therefore is required "by default".
+    - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well.
+
+
+## REST Server
+
+Create a new REST server with its own target routes.
+
+- Protocol identifier: `rest`
+- URI scheme: `rest://localhost[:port]/<path>`
+- Default port: 4567
+- Type for mapping definitions: `byte[]`
+- Required runtime dependencies:
+    - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'`
+- Receive behaviour: Upon connection, create a new PUT connection and pass the value of every call to this PUT route to the receiving port.
+- Send behaviour: Upon connection, create a new GET connection and serve the latest value at this GET route.
+- Additional remarks:
+    - Host is always `localhost`.
+    - Targets to be invoked need to replace `rest` with `http`
+    - Might work with newer versions of `com.sparkjava.spark-core` as well.
+    - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j].
+
+
+## REST Client
+
+Invoke REST routes to fetch and send values.
+
+- Protocol identifier: `restClient`
+- URI scheme: `restClient://localhost[:port]/<path>`
+- Default port: 80
+- Type for mapping definitions: `byte[]`
+- No required runtime dependencies
+- Receive behaviour: Whenever the accessor is called, a GET request fetches the latest data and returns this data.
+- Send behaviour: When the value to be sent changes, a PUT request sends this data.
+- Additional remarks:
+    - Invoked target replaces `restClient` with `http`
+    - **Important constraint**: Receiving ports are only supported for tokens, since they interrupt the getter method!
+
+[slf4j]: https://www.slf4j.org/manual.html
diff --git a/pages/mkdocs.yml b/pages/mkdocs.yml
index 15c561496d48591b4d5b66049646633b142fe3ef..21b10b06e5bb470225199e1f3b7481e72fb55001 100644
--- a/pages/mkdocs.yml
+++ b/pages/mkdocs.yml
@@ -7,6 +7,7 @@ nav:
   - "Using RagConnect (by Example)": using.md
   - "RagConnect Specification Language": dsl.md
   - "Compiler options": compiler.md
+  - "Communication Protocols": handlers.md
   - "Use Cases": use_cases.md
   - "Inner workings": inner-workings.md
   - "Evaluation Metrics: Lines of Code": cloc.md
diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag
index e1ec0f28977748a6b2d896c0c82c58c116f0081c..407efa363aa6208730add0899d650a2cd73a497a 100644
--- a/ragconnect.base/src/main/jastadd/Analysis.jrag
+++ b/ragconnect.base/src/main/jastadd/Analysis.jrag
@@ -112,7 +112,7 @@ aspect Analysis {
     return !getDependencySourceDefinitionList().isEmpty() ||
               getTokenPortTargetList().stream()
                 .map(PortTarget::containingPortDefinition)
-                .anyMatch(PortDefinition::shouldNotResetValue);
+                .anyMatch(def -> def.shouldNotResetValue() || ragconnect().restClientHandler().getInUse());
   }
 
   // --- effectiveUsedAt ---
diff --git a/ragconnect.base/src/main/jastadd/Handlers.jrag b/ragconnect.base/src/main/jastadd/Handlers.jrag
index e64a8c5d0db5467f9a4b0d7451e8c38ab22a8bbf..8d891d5025877f387425803fa39e930a7b023314 100644
--- a/ragconnect.base/src/main/jastadd/Handlers.jrag
+++ b/ragconnect.base/src/main/jastadd/Handlers.jrag
@@ -2,6 +2,7 @@ aspect RagConnectHandlers {
   syn Handler RagConnect.javaHandler() = resolveHandlerByName("java");
   syn Handler RagConnect.mqttHandler() = resolveHandlerByName("mqtt");
   syn Handler RagConnect.restHandler() = resolveHandlerByName("rest");
+  syn Handler RagConnect.restClientHandler() = resolveHandlerByName("restClient");
 
   private Handler RagConnect.resolveHandlerByName(String uniqueName) {
     for (Handler handler : getHandlerList()) {
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index 60fe493797256563e7bfca17a6c35d341acfb8c0..ebf3b0af00dcb9f6acf7dd95cd658cdb0ead5a49 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -232,7 +232,8 @@ aspect MustacheMappingApplicationAndDefinition {
   eq MRelationSendDefinition.preemptiveReturn() = "return false;";
 
   eq MTokenReceiveDefinition.firstInputVarName() = "message";
-  eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall();
+  // if receiverName variable is set, use internal getter instead to avoid StackOverflow
+  eq MTokenReceiveDefinition.preemptiveExpectedValue() = "(" + getPortDefinition().receiverName() + " != null ? get" + getPortDefinition().token().internalName() + "() : " + getterMethodCall() + ")";
   eq MTokenReceiveDefinition.preemptiveReturn() = "return;";
 
   eq MTokenSendDefinition.firstInputVarName() = getterMethodCall();
@@ -525,6 +526,7 @@ aspect MustacheSendDefinition {
 
   syn boolean PortDefinition.relationPortWithListRole() = getPortTarget().relationPortWithListRole();
 
+  syn String PortDefinition.receiverName() = getPortTarget().receiverName();
   syn String PortDefinition.senderName() = getPortTarget().senderName();
 
   syn java.util.List<SendIncrementalObserverEntry> PortDefinition.sendIncrementalObserverEntries() {
@@ -583,6 +585,9 @@ containingPortDefinition().getIndexBasedListAccess());
   syn boolean PortTarget.relationPortWithListRole() = false;
   eq RelationPortTarget.relationPortWithListRole() = getRole().isListRole();
 
+  syn String PortTarget.receiverName() = ragconnect().internalRagConnectPrefix() + "_receiver_" + entityName();
+  eq ContextFreeTypePortTarget.receiverName() = null;
+
   syn String PortTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName();
   eq ContextFreeTypePortTarget.senderName() = null;
 
@@ -645,6 +650,16 @@ aspect MustacheTokenComponent {
 
   syn String TokenComponent.javaType() = effectiveJavaTypeUse().prettyPrint();
 
+  syn PortDefinition TokenComponent.normalTokenReceiveDef() {
+    for (Component comp : meOwnedByOthers()) {
+      PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenReceiveDef();
+      if (maybeResult != null) {
+        return maybeResult;
+      }
+    }
+    return directNormalTokenReceiveDef();
+  }
+
   syn PortDefinition TokenComponent.normalTokenSendDef() {
     for (Component comp : meOwnedByOthers()) {
       PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenSendDef();
@@ -682,6 +697,16 @@ aspect MustacheTokenComponent {
   // > see MustacheSend for updateMethodName, writeMethodName
 
   // === attributes needed for computing above ones ===
+  syn PortDefinition TokenComponent.directNormalTokenReceiveDef() {
+    for (PortTarget target : getTokenPortTargetList()) {
+      if (target.isTokenPortTarget() && !target.containingPortDefinition().getSend() &&
+              ragconnect().restClientHandler().getInUse()) {
+        return target.containingPortDefinition();
+      }
+    }
+    return null;
+  }
+
   syn PortDefinition TokenComponent.directNormalTokenSendDef() {
     for (PortTarget target : getTokenPortTargetList()) {
       if (target.isTokenPortTarget() && target.containingPortDefinition().shouldNotResetValue()) {
diff --git a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
index 5aa0f8c5fb1d007e6dfaca1b0fd70cf7566d810b..c02518e3658395beffa69c3f5ba6983b826de62d 100644
--- a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
+++ b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
@@ -233,6 +233,15 @@ aspect IntermediateToYAML {
     }
     return sb;
   }
+
+  // FIXME: remove refine once fixed in upstream mustache
+  refine Helpers protected SimpleElement ComplexElement.makeStringElement(String value) {
+    // simple test, check for special characters
+    return containsAny(value, ":#,[{\"\n") ?
+      StringElement.of(value.replace("\n", "\\n").replace("\"", "\\\"")) :
+      ValueElement.of(value);
+  }
+
 }
 
 aspect Navigation {
diff --git a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
index be81bfd4ea84123624e97c0047b85e7b29bda348..173404cee21fdb497276bc99c169dbe0c5864e58 100644
--- a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
+++ b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
@@ -39,6 +39,7 @@ public class Compiler extends AbstractCompiler {
   private static final String OPTION_PROTOCOL_JAVA = "java";
   private static final String OPTION_PROTOCOL_MQTT = "mqtt";
   private static final String OPTION_PROTOCOL_REST = "rest";
+  private static final String OPTION_PROTOCOL_REST_CLIENT = "restClient";
 
   public Compiler() {
     super("ragconnect", true);
@@ -183,6 +184,7 @@ public class Compiler extends AbstractCompiler {
             .addDefaultValue(OPTION_PROTOCOL_MQTT, "Enable MQTT")
             .addAcceptedValue(OPTION_PROTOCOL_JAVA, "Enable Java (experimental)")
             .addAcceptedValue(OPTION_PROTOCOL_REST, "Enable REST")
+            .addAcceptedValue(OPTION_PROTOCOL_REST_CLIENT, "Enable REST client (experimental)")
     );
     optionPrintYaml = addOption(
         new BooleanOption("printYaml", "Print out YAML instead of generating files and exit.")
@@ -341,6 +343,8 @@ public class Compiler extends AbstractCompiler {
     ragConnect.addHandler(new Handler("JavaHandler", "java", optionProtocols.hasValue(OPTION_PROTOCOL_JAVA)));
     ragConnect.addHandler(new Handler("MqttServerHandler", "mqtt", optionProtocols.hasValue(OPTION_PROTOCOL_MQTT)));
     ragConnect.addHandler(new Handler("RestServerHandler", "rest", optionProtocols.hasValue(OPTION_PROTOCOL_REST)));
+    ragConnect.addHandler(new Handler("RestClientHandler", "restClient",
+            optionProtocols.hasValue(OPTION_PROTOCOL_REST_CLIENT)));
   }
 
   public String generateAspect(RagConnect ragConnect) {
diff --git a/ragconnect.base/src/main/resources/RestClientHandler.mustache b/ragconnect.base/src/main/resources/RestClientHandler.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..24307ef7c8e22274bf71929774a8f7e12521b216
--- /dev/null
+++ b/ragconnect.base/src/main/resources/RestClientHandler.mustache
@@ -0,0 +1,65 @@
+public class RestClientHandler {
+  private java.net.http.HttpClient httpClient;
+  public RestClientHandler(String name) {
+    httpClient = java.net.http.HttpClient.newHttpClient();
+  }
+
+  public RagConnectReceiver newReceiverFor(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> callback) {
+    java.net.URI target = java.net.URI.create(connectToken.uri.toString().replaceFirst("restClient", "http"));
+    java.net.http.HttpRequest httpRequest = java.net.http.HttpRequest.newBuilder(target)
+            .GET()
+            .build();
+    return () -> {
+      byte[] rawInput;
+      try {
+        rawInput = fetchFrom(httpRequest);
+      } catch (Exception e) {
+        {{logException}}("Exception when fetching from " + target, e);
+        return;
+      }
+      callback.accept("", rawInput);
+    };
+  }
+
+  byte[] fetchFrom(java.net.http.HttpRequest httpRequest) throws java.io.IOException, InterruptedException {
+    return httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofByteArray()).body();
+  }
+
+  public RestClientPreparedRequest preparePut(java.net.URI uri) {
+    java.net.URI target = java.net.URI.create(uri.toString().replaceFirst("restClient", "http"));
+    java.net.http.HttpRequest.Builder httpRequestBuilder = java.net.http.HttpRequest.newBuilder(target);
+    return new RestClientPreparedRequest(httpRequestBuilder);
+  }
+
+  void sendRequestAsync(RestClientPreparedRequest preparedRequest, byte[] message) {
+    java.net.http.HttpRequest httpRequest = preparedRequest.builder
+            .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message))
+            .build();
+    httpClient.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.discarding());
+  }
+
+  void sendRequestSync(RestClientPreparedRequest preparedRequest, byte[] message) {
+    java.net.http.HttpRequest httpRequest = preparedRequest.builder
+            .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message))
+            .build();
+    java.net.http.HttpResponse response;
+    try {
+      response = httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofString());
+      {{logDebug}}("Response for message to {{log_}} is {{log_}} {{log_}}",
+              httpRequest, response.statusCode(), response.body());
+    } catch (java.io.IOException | InterruptedException e) {
+      {{logException}}("Exception while sending {{log_}}: {{log_}}", httpRequest, e);
+    }
+  }
+
+  public void close() {
+    // empty
+  }
+}
+
+public class RestClientPreparedRequest {
+  java.net.http.HttpRequest.Builder builder;
+  RestClientPreparedRequest(java.net.http.HttpRequest.Builder builder) {
+    this.builder = builder;
+  }
+}
diff --git a/ragconnect.base/src/main/resources/RestHandler.mustache b/ragconnect.base/src/main/resources/RestHandler.mustache
index 25192af366bfd4da29ace5c11d661b8cc8fd0992..4e8f228712fd7b6e6ee3c66e72f85c8d8ac5dd9b 100644
--- a/ragconnect.base/src/main/resources/RestHandler.mustache
+++ b/ragconnect.base/src/main/resources/RestHandler.mustache
@@ -20,13 +20,13 @@ public class RestServerHandler {
     return handler;
   }
 
-  public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.Consumer<String> callback) {
-    tokensForRemoval.put(connectToken, callback);
-    resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), callback);
+  public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> consumer) {
+    tokensForRemoval.put(connectToken, consumer);
+    resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), consumer);
     return true;
   }
 
-  public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<String> supplier) {
+  public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<byte[]> supplier) {
     tokensForRemoval.put(connectToken, supplier);
     resolveHandler(connectToken.uri).newGETConnection(connectToken.uri.getPath(), supplier);
     return true;
@@ -52,8 +52,8 @@ public class RestHandler {
 
   private int port;
   /** Dispatch knowledge */
-  private final java.util.Map<String, java.util.List<java.util.function.Consumer<String>>> callbacks;
-  private final java.util.Map<String, SupplierWithException<String>> suppliers;
+  private final java.util.Map<String, java.util.List<java.util.function.BiConsumer<String, byte[]>>> callbacks;
+  private final java.util.Map<String, SupplierWithException<byte[]>> suppliers;
 
   public RestHandler() {
     this.port = DEFAULT_PORT;
@@ -64,23 +64,24 @@ public class RestHandler {
   public RestHandler setPort(int port) {
     this.port = port;
     start();
+    spark.Spark.path("", () -> spark.Spark.before((request, response) -> {{logDebug}}("Request on {{log_}}: {{log_}}", request.pathInfo(), request.bodyAsBytes())));
     return this;
   }
 
-  public void newPUTConnection(String path, java.util.function.Consumer<String> callback) {
+  public void newPUTConnection(String path, java.util.function.BiConsumer<String, byte[]> callback) {
     if (callbacks.containsKey(path)) {
       callbacks.get(path).add(callback);
     } else {
       // setup path
-      java.util.List<java.util.function.Consumer<String>> callbackList = new java.util.ArrayList<>();
+      java.util.List<java.util.function.BiConsumer<String, byte[]>> callbackList = new java.util.ArrayList<>();
       callbackList.add(callback);
       callbacks.put(path, callbackList);
       spark.Spark.put(path, (request, response) -> {
-        String content = request.body();
+        byte[] content = request.bodyAsBytes();
         java.util.Set<String> errors = new java.util.HashSet<>();
-        for (java.util.function.Consumer<String> f : callbackList) {
+        for (java.util.function.BiConsumer<String, byte[]> f : callbackList) {
           try {
-            f.accept(content);
+            f.accept(path, content);
           } catch (Exception e) {
             errors.add(e.getMessage());
           }
@@ -94,7 +95,7 @@ public class RestHandler {
     }
   }
 
-  public void newGETConnection(String path, SupplierWithException<String> supplier) {
+  public void newGETConnection(String path, SupplierWithException<byte[]> supplier) {
     if (suppliers.get(path) != null) {
       {{logWarn}}("Overriding existing supplier for '{{log_}}'", path);
     }
diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache
index 75bd14ce26638ceeee267251988a01b516de0970..435a88c8c765ed35fe72caeca96ca1770b3f0968 100644
--- a/ragconnect.base/src/main/resources/handler.mustache
+++ b/ragconnect.base/src/main/resources/handler.mustache
@@ -46,6 +46,12 @@ aspect RagConnectHandler {
   {{/InUse}}
 {{/restHandler}}
 
+{{#restClientHandler}}
+  {{#InUse}}
+  {{> RestClientHandler}}
+  {{/InUse}}
+{{/restClientHandler}}
+
   class RagConnectToken {
     static java.util.concurrent.atomic.AtomicLong counter = new java.util.concurrent.atomic.AtomicLong(0);
     final long id;
@@ -191,4 +197,7 @@ aspect RagConnectHandler {
       java.util.Optional.ofNullable(publishers.get(index)).ifPresent(publisher -> publisher.setLastValue(value));
     }
   }
+
+  interface RagConnectReceiver extends Runnable {
+  }
 }
diff --git a/ragconnect.base/src/main/resources/receiveDefinition.mustache b/ragconnect.base/src/main/resources/receiveDefinition.mustache
index b2e05ce6b2ce246b5225b98eecc7779bf3b0cfa0..f1ca58d6a02ba1dcfd3b47cb62e1daa6c92fd195 100644
--- a/ragconnect.base/src/main/resources/receiveDefinition.mustache
+++ b/ragconnect.base/src/main/resources/receiveDefinition.mustache
@@ -1,3 +1,5 @@
+private RagConnectReceiver {{parentTypeName}}.{{receiverName}} = null;
+
 {{#typeIsList}}
 {{#IndexBasedListAccess}}
 private int {{parentTypeName}}.{{resolveInListMethodName}}(String topic) {
@@ -116,13 +118,18 @@ private boolean {{parentTypeName}}.{{internalConnectMethodName}}(String {{connec
   {{#restHandler}}
   {{#InUse}}
     case "rest":
-      success = {{attributeName}}().newPUTConnection(connectToken, input -> {
-        // TODO wildcard-topic not supported yet
-        consumer.accept("", input.getBytes());
-      });
+      success = {{attributeName}}().newPUTConnection(connectToken, consumer);
       break;
   {{/InUse}}
   {{/restHandler}}
+  {{#restClientHandler}}
+  {{#InUse}}
+    case "restClient":
+      {{receiverName}} = {{attributeName}}().newReceiverFor(connectToken, consumer);
+      success = {{receiverName}} != null;
+      break;
+  {{/InUse}}
+  {{/restClientHandler}}
     default:
       {{logError}}("Unknown protocol '{{log_}}'.", scheme);
       success = false;
@@ -161,6 +168,15 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam
     break;
   {{/InUse}}
   {{/restHandler}}
+  {{#restClientHandler}}
+  {{#InUse}}
+    case "restClient": disconnectingMethod = token -> {
+      {{receiverName}} = null;
+      return true;
+    };
+    break;
+  {{/InUse}}
+  {{/restClientHandler}}
     default:
       {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}",
         scheme, {{connectParameterName}});
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index d97618567dce78a10b56680aac09d31e5b06c74a..45fa4de131a4331827e2643d04d51757c61e1fb2 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -54,12 +54,28 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
     case "rest": {
       success = {{attributeName}}().newGETConnection(connectToken, () -> {
         {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
-        return new String({{lastValueGetterCall}});
+        return {{lastValueGetterCall}};
       });
       break;
     }
   {{/InUse}}
   {{/restHandler}}
+  {{#restClientHandler}}
+  {{#InUse}}
+    case "restClient":
+      RestClientPreparedRequest preparedRequest = {{attributeName}}().preparePut(uri);
+      {{senderName}}.add(() -> {
+        {{! sync or async could be selected via options, see issue #62 }}
+        {{attributeName}}().sendRequestSync(preparedRequest, {{lastValueGetterCall}});
+        }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken);
+      {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
+      if (writeCurrentValue) {
+        {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken);
+      }
+      success = true;
+      break;
+  {{/InUse}}
+  {{/restClientHandler}}
     default:
       {{logError}}("Unknown protocol '{{log_}}'.", scheme);
       success = false;
@@ -121,6 +137,13 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam
       break;
   {{/InUse}}
   {{/restHandler}}
+  {{#restClientHandler}}
+  {{#InUse}}
+    case "restClient":
+      disconnectingMethod = {{senderName}}::remove;
+      break;
+  {{/InUse}}
+  {{/restClientHandler}}
     default:
       {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}",
         scheme, {{connectParameterName}});
@@ -165,9 +188,6 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i
 {{#needForwarding}}
 syn {{{forwardingType}}} {{parentTypeName}}.{{forwardingName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) {
 {{#relationPortWithListRole}}
-//  for (var element : {{realGetterMethodCall}}) {
-//    element.{{touchedTerminalsMethodName}}();
-//  }
   {{realGetterMethodCall}}.stream().forEach(element -> element.{{touchedTerminalsMethodName}}());
   return {{realGetterMethodCall}};
 {{/relationPortWithListRole}}
diff --git a/ragconnect.base/src/main/resources/tokenComponent.mustache b/ragconnect.base/src/main/resources/tokenComponent.mustache
index 3d90c34b5ccfd3844972ef535844f8b042e1e3f1..ee669d87bf128bcecdd2a2830586bd26df83921f 100644
--- a/ragconnect.base/src/main/resources/tokenComponent.mustache
+++ b/ragconnect.base/src/main/resources/tokenComponent.mustache
@@ -18,5 +18,10 @@ public {{parentTypeName}} {{parentTypeName}}.set{{Name}}({{javaType}} value) {
 }
 
 public {{javaType}} {{parentTypeName}}.get{{Name}}() {
+  {{#normalTokenReceiveDef}}
+  if ({{receiverName}} != null) {
+    {{receiverName}}.run();
+  }
+  {{/normalTokenReceiveDef}}
   return get{{internalName}}();
 }
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index fb5e710ae4d4873e37e4454c0adccccc26750838..255091abca95394c7245be0d67564cd08d5a5328 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -712,6 +712,28 @@ task compileJavaIncremental(type: RagConnectTest) {
     }
 }
 
+// --- Test: rest-client-server ---
+task compileRestClientServerTest(type: RagConnectTest) {
+    ragconnect {
+        outputDir = file('src/test/02-after-ragconnect/restClientServer')
+        inputFiles = [file('src/test/01-input/restClientServer/Test.relast'),
+                      file('src/test/01-input/restClientServer/Test.connect')]
+        rootNode = 'Root'
+        protocols = ['rest','restClient']
+        extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329'])
+    }
+    relast {
+        useJastAddNames = true
+        grammarName = 'src/test/03-after-relast/restClientServer/restClientServer'
+        serializer = 'jackson'
+    }
+    jastadd {
+        jastAddList = 'JastAddList'
+        packageName = 'restClientServer.ast'
+        extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
+    }
+}
+
 // --- Task order ---
 classes.dependsOn(':ragconnect.base:jar')
 
diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.connect b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect
new file mode 100644
index 0000000000000000000000000000000000000000..cbee90ac4a7e4bcc2209035a1e77b75abf16d2d2
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect
@@ -0,0 +1,5 @@
+send SenderRoot.SimpleValue ;
+receive ReceiverRoot.ReceivedValue ;
+
+send SenderRoot.SingleA ;
+receive ReceiverRoot.SomeA ;
diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.relast b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast
new file mode 100644
index 0000000000000000000000000000000000000000..5c7bcdeb79b3df51ec142093e7b920b0e04e8ae4
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast
@@ -0,0 +1,5 @@
+Root ::= SenderRoot ReceiverRoot;
+SenderRoot ::= <SimpleValue:int> SingleA:A ;
+ReceiverRoot ::=  <ReceivedValue:int> SomeA:A ;
+A ::= <Value> Inner ;
+Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bcfaea91118d671f8a65275e1abcf364862036a0
--- /dev/null
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java
@@ -0,0 +1,124 @@
+package org.jastadd.ragconnect.tests;
+
+import org.jastadd.ragconnect.tests.utils.TestChecker;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import restClientServer.ast.*;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Testing RestClient.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class RestClientServerTest extends RagConnectTest {
+  private Root root;
+  private TestChecker checker;
+
+  private static final String TOPIC_NT_A_VALUE = "nt/a/value";
+  private static final String TOPIC_NT_A_INNER_VALUE = "nt/a/inner/value";
+
+  @Test
+  public void testSimpleSenderRest() throws IOException {
+    root.getSenderRoot().setSimpleValue(1);
+
+    assertThat(root.getReceiverRoot().connectReceivedValue("restClient://localhost:4567/serve-simple")).isTrue();
+    assertThat(root.getSenderRoot().connectSimpleValue("rest://localhost:4567/serve-simple", true)).isTrue();
+
+    assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1);
+
+    root.getSenderRoot().setSimpleValue(2);
+    assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2);
+  }
+
+  @Test
+  public void testSimpleSenderRestClient() throws IOException {
+    root.getSenderRoot().setSimpleValue(1);
+
+    assertThat(root.getReceiverRoot().connectReceivedValue("rest://localhost:4567/put-simple")).isTrue();
+    assertThat(root.getSenderRoot().connectSimpleValue("restClient://localhost:4567/put-simple", true)).isTrue();
+
+    assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1);
+
+    root.getSenderRoot().setSimpleValue(2);
+    assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2);
+  }
+
+  @Test
+  public void testNonterminalSenderRest() throws IOException {
+    A a = new A().setValue("a1");
+    a.setInner(new Inner().setInnerValue("1"));
+    root.getSenderRoot().setSingleA(a);
+
+    checker.put(TOPIC_NT_A_VALUE, "a1")
+            .put(TOPIC_NT_A_INNER_VALUE, "1");
+
+    assertThat(root.getReceiverRoot().connectSomeA("rest://localhost:4567/put-nt")).isTrue();
+    assertThat(root.getSenderRoot().connectSingleA("restClient://localhost:4567/put-nt", true)).isTrue();
+
+    communicateNonterminal();
+  }
+
+  @Test
+  @Disabled("Receiving nonterminals using restClient is not supported yet")
+  public void testNonterminalSenderRestClient() throws IOException {
+    A a = new A().setValue("a1");
+    a.setInner(new Inner().setInnerValue("1"));
+    root.getSenderRoot().setSingleA(a);
+
+    assertThat(root.getReceiverRoot().connectSomeA("restClient://localhost:4567/serve-nt")).isTrue();
+    assertThat(root.getSenderRoot().connectSingleA("rest://localhost:4567/serve-nt", true)).isTrue();
+
+    communicateNonterminal();
+  }
+
+  private void communicateNonterminal() {
+    A a = root.getSenderRoot().getSingleA();
+
+    checker.check();
+
+    a.setValue("a23");
+    checker.put(TOPIC_NT_A_VALUE, "a23").check();
+
+    a.getInner().setInnerValue("abc");
+    checker.put(TOPIC_NT_A_INNER_VALUE, "abc").check();
+  }
+
+  @BeforeEach
+  public void createModel() {
+    root = new Root();
+    root.setReceiverRoot(new ReceiverRoot());
+    root.setSenderRoot(new SenderRoot());
+
+    checker = new TestChecker().setActualNumberOfValues(() -> 0);
+    checker.setCheckForString(TOPIC_NT_A_VALUE, (name, expected) ->
+                    assertThat(someAValue()).describedAs(name).isEqualTo(expected))
+            .setCheckForString(TOPIC_NT_A_INNER_VALUE, (name, expected) ->
+                    assertThat(someAInnerValueOrNull()).describedAs(name).isEqualTo(expected))
+    ;
+  }
+
+  private String someAValue() {
+    if (root.getReceiverRoot().getSomeA() == null) {
+      return null;
+    }
+    return root.getReceiverRoot().getSomeA().getValue();
+  }
+
+  private String someAInnerValueOrNull() {
+    if (root.getReceiverRoot().getSomeA() == null || root.getReceiverRoot().getSomeA().getInner() == null) {
+      return null;
+    }
+    return root.getReceiverRoot().getSomeA().getInner().getInnerValue();
+  }
+
+  @AfterEach
+  public void close() {
+    root.ragconnectCloseConnections();
+  }
+}