Hands-on with deepstream.io: build realtime apps with less backend code

The developers at Hoxton One recently released deepstream, a new open source framework for realtime web application development. The framework, which is built with Node.js and Engine.io, helps developers build frontend web applications that perform realtime updates while requiring minimal backend code.

The deepstream framework comes with a client library for the browser that lets the developer create and update data records on the fly. The client library relays updates to the server, which propagates the new data to other subscribed clients. The developer doesn’t have to write any specialized backend code to handle the changes or propagate the events.

The developers behind deepstream provide an optional RethinkDB data connector, which lets the framework use RethinkDB for data persistence. As the frontend client updates records, the deepstream backend stores the changes in RethinkDB documents.

To get a hands-on look at deepstream, I used the framework to build a very simple todo list application. In this blog post, I’ll show you my demo app and demonstrate how to configure deepstream to use the RethinkDB storage connector.

Configure deepstream to use RethinkDB

On the backend, I set up a deepstream server in Node.js. I installed the deepstream.io and deepstream.io-storage-rethinkdb packages from NPM to get the underlying framework and the RethinkDB storage connector. Next, I wrote the following script to configure the server:

var DSServer = require("deepstream.io");
var DSRethinkConnector = require("deepstream.io-storage-rethinkdb");

// Setup the deepstream server
var server = new DSServer();
server.set("host", "localhost");
server.set("port", 6020);

// Setup the RethinkDB storage connector
server.set("storage", new DSRethinkConnector({
    port: 28015,
    host: "localhost",
    splitChar: "/",
    defaultTable: "dsdemo"
}));

// Run the server
server.start();

If your RethinkDB cluster isn’t running on the same server as the application, be sure to specify the correct host when you instantiate the connector. As you can see, you don’t need much code to get a deepstream backend up and running. You can run the script from the command line to start the server.

Connect on the frontend

To connect on the frontend, the web page will need to load the deepstream client library. You can install the client library from bower or simply obtain the raw, minified JavaScript. The following example shows how to build a simple web page that connects to the deepstream server, creates a new record, and monitors for updates:

<html>
<head>
  <script src="bower_components/deepstream.io-client-js/dist/deepstream.min.js"></script>
</head>
<body>
...
<script>
  // Connect to the deepstream server
  var ds = deepstream("localhost:6020").login();

  // Create a unique name for the new record
  var name = "myrecords/" + ds.getUid();

  // Instantiate a new record
  var record = ds.record.getRecord(name);

  // Set several properties of the new record
  record.set({
    name: "Enterprise",
    registry: "NCC-1701",
    category: "Constitution",
    crew: 430
  });

  // Subscribe to changes on the `crew` property
  record.subscribe("crew", function(value) {
    console.log("Crew count updated:", value);
  });
</script>
</body>
</html>

Each deepstream record has a unique ID, typically comprised of a path and random characters. When you fetch a non-existent record by ID, the framework will create a new record. The set method applies values to record properties. When the user invokes the set method, the client transmits the updated values to the server so that they can be propagated to other clients.

The subscribe method allows you to track updates on a record. You can subscribe to changes on an individual property (as demonstrated above) or you can track changes on all of the properties of an individual record. The client library will invoke the provided callback when it detects a change.

In addition to records, deepstream also supports observable collections. You can create a list of records and subscribe to see when the application adds and removes items. I used a deepstream list in my demo application to track the items included in the todo list.

Build a realtime todo list

I built my deepstream todo list demo with Vue, a lightweight JavaScript MVC framework. In order to make Vue’s data bindings work with deepstream records, I made a simple wrapper function that subscribes to changes on the record and then applies those changes with Vue’s $set method:

function wrapRecord(record) {
  record.subscribe(function(data) {
    for (var prop in data)
      if (data.hasOwnProperty(prop))
        record.$set(prop, data[prop]);
  });

  return record;
}

When I apply that function to a deepstream record in my Vue application, it will ensure that user interface bindings instantly reflect changes that propagate from other users. It only works one way, however.

Each item in the todo list has a checkbox and a label. The complete user interface includes a repeating series of todo list items and an input box that allows users to add new items:

<div id="todolist">
  <ul>
    <li v-repeat="items" v-on="click: toggle">
      <label>
        <input type="checkbox" checked="{{done}}">
        {{text}}
      </label>
    </li>
  </ul>

  <input type="text" v-model="newItemName" placeholder="New Item">
  <button v-on="click: newItem">Add</button>
</div>

The corresponding Vue controller populates the user interface with existing items, adds a new item when the user clicks the “Add” button, and broadcasts the update when the user toggles a checkbox in the list:

// Connect to the deepstream server
var ds = deepstream("localhost:6020").login();

// Fetch the list of todo items
var list = ds.record.getList("todos");

function wrapRecord(record) {
  record.subscribe(function(data) {
    for (var prop in data)
      if (data.hasOwnProperty(prop))
        record.$set(prop, data[prop]);
  });

  return record;
}

var todo = new Vue({
  el: "#todolist",
  data: { items: [] },
  methods: {
    toggle: function(ev) {
      // Set the `done` property when the user toggles a checkbox
      if (ev.target.checked != undefined)
        todo.items[ev.targetVM.$index].set("done", ev.target.checked);
    },
    newItem: function() {
      // Create a name and record for the new todo item
      var name = "todos/" + ds.getUid();
      var record = ds.record.getRecord(name);

      // Use the input text as the todo list item's label
      record.set({text: this.newItemName, done: false});
      // Add the new record to the list
      list.addEntry(name);
    },
  },
  ready: function() {
    // Iterate over the items in the list...
    list.subscribe(function(data) {
      data.forEach(function(name) {
        // Fetch the associated deepstream record
        var record = ds.record.getRecord(name);
        record.whenReady(function() {
          // Wrap it and add it to the todo list
          todo.items.push(wrapRecord(record));
        });
      });
    });
  }
});

In the example above, the list.subscribe callback executes every time a user adds new records to the list. It also executes once when the application first starts, passing in the entire series so that I can populate the list with existing items.

In the toggle event handler, I manually invoke the set method for the record in order to update the done property with the status of the checkbox.

The newItem event handler creates a new record and adds it to the list. When I invoke the list.addEntry method, the application automatically triggers the callback on the list subscription, ensuring that the new item appears in the user interface.

Observe updates with changefeeds

When you use the RethinkDB connector with deepstream, you can externally access the data with conventional ReQL queries. You can even use RethinkDB changefeeds to monitor the changes in realtime. The deepstream framework creates a separate table for each of your collections. You can use the following query in the RethinkDB Data Explorer to see the data from the todo list demo:

r.db("deepstream").table("todos")

If you add the changes command to the end, you can watch for updates in realtime. When a user adds a new item or toggles a checkbox, you will see an update in the data explorer. Unfortunately, you can’t make live changes to deepstream records with ReQL. The framework has its own approach to handling updates, so changes won’t properly propagate to clients if you update documents outside of the framework.

Next steps

Deepstream made it possible for me to build a realtime todo list without an application-specific backend code. In this case, I used the Node.js backend script solely to configure the ports and attach the RethinkDB data connector. Of course, there are a number of advanced features offered by deepstream that you can use on the backend.

For example, deepstream provides backend APIs for permissions and authentication, which you can optionally use to implement granular access control for record access and updates. The framework also has an RPC system that you can use to invoke methods across the realtime bridge. For more details, be sure to visit the deepstream documentation.

If you would like to try deepstream yourself, consider installing RethinkDB. To learn more about RethinkDB, check out our ten-minute guide.

Resources: