diff --git a/rest_example/config/install/rest.resource.rest_example_product.yml b/rest_example/config/install/rest.resource.rest_example_product.yml
new file mode 100644
index 0000000..c0488ac
--- /dev/null
+++ b/rest_example/config/install/rest.resource.rest_example_product.yml
@@ -0,0 +1,16 @@
+langcode: en
+status: true
+dependencies: {  }
+id: rest_example_product
+plugin_id: rest_example_product
+granularity: resource
+configuration:
+  methods:
+    - GET
+    - POST
+    - PATCH
+    - DELETE
+  formats:
+    - json
+  authentication:
+    - cookie
diff --git a/rest_example/css/rest-example.css b/rest_example/css/rest-example.css
new file mode 100644
index 0000000..68457c4
--- /dev/null
+++ b/rest_example/css/rest-example.css
@@ -0,0 +1,26 @@
+.re-filter input[type="search"]::-webkit-search-cancel-button {
+  -webkit-appearance: searchfield-cancel-button;
+  cursor: pointer;
+}
+
+.re-actions-column {
+  width: 18%;
+}
+
+#re-app td .dropbutton-multiple {
+  padding-right: 0;
+  margin-right: 0;
+}
+
+.re-transition-enter-active {
+  transition: all .3s ease;
+}
+
+.re-transition-enter {
+  padding-left: 10px;
+  opacity: 0;
+}
+.re-transition-leave,
+.re-transition-leave-active {
+  display: none;
+}
diff --git a/rest_example/js/.gitignore b/rest_example/js/.gitignore
new file mode 100644
index 0000000..07e6e47
--- /dev/null
+++ b/rest_example/js/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/rest_example/js/app.es6.js b/rest_example/js/app.es6.js
new file mode 100644
index 0000000..3e3bd20
--- /dev/null
+++ b/rest_example/js/app.es6.js
@@ -0,0 +1,143 @@
+/**
+ * @file
+ * Rest example application.
+ */
+
+(function (settings, t, ProductStore) {
+
+  const productStore = new ProductStore(settings.token);
+  let cachedProducts;
+
+  /* global Vue */
+  Vue.directive('t', {
+    bind: el => {
+      el.innerHTML = t(el.innerHTML.trim());
+    }
+  });
+
+  function goToOverview() {
+    cachedProducts = null;
+    router.push({name: 'overview'});
+  }
+
+  const List = {
+    template: '#re-list',
+    data() {
+      return {
+        filter: '',
+        filterPlaceholder: t('Search by title'),
+        products: [],
+        permissions: settings.permissions,
+        showActions: settings.permissions.patch && settings.permissions.delete
+      };
+    },
+    beforeRouteEnter(to, from, next) {
+      if (cachedProducts) {
+        next(vc => {vc.products = cachedProducts})
+      }
+      else {
+        productStore.getAll((products) => {
+          next(vc => {vc.products = cachedProducts = products})
+        });
+      }
+    },
+    updated() {
+      // Make sure dropbutton.js processes our buttons.
+      Drupal.attachBehaviors(document, drupalSettings);
+    },
+    computed: {
+      filteredProducts() {
+        let filter = this.filter.toLowerCase();
+        return this.products.filter(product => {
+          return !filter || product.title.toLowerCase().indexOf(filter) !== -1;
+        });
+      }
+    }
+  };
+
+  const AddForm = {
+    template: '#re-product-form',
+    data() {
+      return {
+        disabled: false,
+        product: {}
+      };
+    },
+    methods: {
+      save() {
+        if (!this.disabled) {
+          productStore.post(this.product, goToOverview);
+          this.disabled = true;
+        }
+      }
+    }
+  };
+
+  const EditForm = {
+    template: '#re-product-form',
+    data() {
+      return {
+        disabled: false,
+        product: {}
+      };
+    },
+    beforeRouteEnter(to, from, next) {
+      productStore.get(to.params.id, product => {
+        next(vc => {vc.product = product})
+      });
+    },
+    methods: {
+      save() {
+        if (!this.disabled) {
+          productStore.patch(this.product, goToOverview);
+          this.disabled = true;
+        }
+      }
+    }
+  };
+
+  const DeleteForm = {
+    template: '#re-product-delete-form',
+    methods: {
+      remove() {
+        productStore.delete(this.$route.params.id, goToOverview);
+      }
+    }
+  };
+
+  const routes = [
+    {name: 'overview', path: '/', component: List},
+    {name: 'add', path: '/product/add', component: AddForm},
+    {name: 'edit', path: '/product/:id', component: EditForm},
+    {name: 'delete', path: '/product/:id/delete', component: DeleteForm}
+  ];
+
+  /* global VueRouter */
+  const router = new VueRouter({routes: routes});
+
+  new Vue({router: router}).$mount('#re-app');
+
+  /* global RestExampleProductStore */
+}(drupalSettings.restExample, Drupal.t, RestExampleProductStore));
+
+// Translatable stings.
+/*
+ Drupal.t('Add product');
+ Drupal.t('Select by title');
+ Drupal.t('ID');
+ Drupal.t('Title');
+ Drupal.t('Available');
+ Drupal.t('Price');
+ Drupal.t('Actions');
+ Drupal.t('Yes');
+ Drupal.t('No');
+ Drupal.t('List additional actions');
+ Drupal.t('Edit');
+ Drupal.t('Delete');
+ Drupal.t('No products were found.');
+ Drupal.t('Description');
+ Drupal.t('Save');
+ Drupal.t('Cancel');
+ Drupal.t('Are you sure you want to delete this product?')
+ Drupal.t('This action cannot be undone.');
+*/
diff --git a/rest_example/js/app.js b/rest_example/js/app.js
new file mode 100644
index 0000000..f631eaa
--- /dev/null
+++ b/rest_example/js/app.js
@@ -0,0 +1,150 @@
+'use strict';
+
+/**
+ * @file
+ * Rest example application.
+ */
+
+(function (settings, t, ProductStore) {
+
+  var productStore = new ProductStore(settings.token);
+  var cachedProducts = void 0;
+
+  /* global Vue */
+  Vue.directive('t', {
+    bind: function bind(el) {
+      el.innerHTML = t(el.innerHTML.trim());
+    }
+  });
+
+  function goToOverview() {
+    cachedProducts = null;
+    router.push({ name: 'overview' });
+  }
+
+  var List = {
+    template: '#re-list',
+    data: function data() {
+      return {
+        filter: '',
+        filterPlaceholder: t('Search by title'),
+        products: [],
+        permissions: settings.permissions,
+        showActions: settings.permissions.patch && settings.permissions.delete
+      };
+    },
+    beforeRouteEnter: function beforeRouteEnter(to, from, next) {
+      if (cachedProducts) {
+        next(function (vc) {
+          vc.products = cachedProducts;
+        });
+      } else {
+        productStore.getAll(function (products) {
+          next(function (vc) {
+            vc.products = cachedProducts = products;
+          });
+        });
+      }
+    },
+    updated: function updated() {
+      // Make sure dropbutton.js processes our buttons.
+      Drupal.attachBehaviors(document, drupalSettings);
+    },
+
+    computed: {
+      filteredProducts: function filteredProducts() {
+        var filter = this.filter.toLowerCase();
+        return this.products.filter(function (product) {
+          return !filter || product.title.toLowerCase().indexOf(filter) !== -1;
+        });
+      }
+    }
+  };
+
+  var AddForm = {
+    template: '#re-product-form',
+    data: function data() {
+      return {
+        disabled: false,
+        product: {}
+      };
+    },
+
+    methods: {
+      save: function save() {
+        if (!this.disabled) {
+          productStore.post(this.product, goToOverview);
+          this.disabled = true;
+        }
+      }
+    }
+  };
+
+  var EditForm = {
+    template: '#re-product-form',
+    data: function data() {
+      return {
+        disabled: false,
+        product: {}
+      };
+    },
+    beforeRouteEnter: function beforeRouteEnter(to, from, next) {
+      productStore.get(to.params.id, function (product) {
+        next(function (vc) {
+          vc.product = product;
+        });
+      });
+    },
+
+    methods: {
+      save: function save() {
+        if (!this.disabled) {
+          productStore.patch(this.product, goToOverview);
+          this.disabled = true;
+        }
+      }
+    }
+  };
+
+  var DeleteForm = {
+    template: '#re-product-delete-form',
+    methods: {
+      remove: function remove() {
+        productStore.delete(this.$route.params.id, goToOverview);
+      }
+    }
+  };
+
+  var routes = [{ name: 'overview', path: '/', component: List }, { name: 'add', path: '/product/add', component: AddForm }, { name: 'edit', path: '/product/:id', component: EditForm }, { name: 'delete', path: '/product/:id/delete', component: DeleteForm }];
+
+  /* global VueRouter */
+  var router = new VueRouter({ routes: routes });
+
+  new Vue({ router: router }).$mount('#re-app');
+
+  /* global RestExampleProductStore */
+})(drupalSettings.restExample, Drupal.t, RestExampleProductStore);
+
+// Translatable stings.
+/*
+ Drupal.t('Add product');
+ Drupal.t('Select by title');
+ Drupal.t('ID');
+ Drupal.t('Title');
+ Drupal.t('Available');
+ Drupal.t('Price');
+ Drupal.t('Actions');
+ Drupal.t('Yes');
+ Drupal.t('No');
+ Drupal.t('List additional actions');
+ Drupal.t('Edit');
+ Drupal.t('Delete');
+ Drupal.t('No products were found.');
+ Drupal.t('Description');
+ Drupal.t('Save');
+ Drupal.t('Cancel');
+ Drupal.t('Are you sure you want to delete this product?')
+ Drupal.t('This action cannot be undone.');
+*/
+
+//# sourceMappingURL=app.js.map
\ No newline at end of file
diff --git a/rest_example/js/app.js.map b/rest_example/js/app.js.map
new file mode 100644
index 0000000..f8d2977
--- /dev/null
+++ b/rest_example/js/app.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["app.es6.js"],"names":["settings","t","ProductStore","productStore","token","cachedProducts","Vue","directive","bind","el","innerHTML","trim","goToOverview","router","push","name","List","template","data","filter","filterPlaceholder","products","permissions","showActions","patch","delete","beforeRouteEnter","to","from","next","vc","getAll","updated","Drupal","attachBehaviors","document","drupalSettings","computed","filteredProducts","toLowerCase","product","title","indexOf","AddForm","disabled","methods","save","post","EditForm","get","params","id","DeleteForm","remove","$route","routes","path","component","VueRouter","$mount","restExample","RestExampleProductStore"],"mappings":"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","file":"app.es6.js","sourcesContent":["/**\n * @file\n * Rest example application.\n */\n\n(function (settings, t, ProductStore) {\n\n  const productStore = new ProductStore(settings.token);\n  let cachedProducts;\n\n  /* global Vue */\n  Vue.directive('t', {\n    bind: el => {\n      el.innerHTML = t(el.innerHTML.trim());\n    }\n  });\n\n  function goToOverview() {\n    cachedProducts = null;\n    router.push({name: 'overview'});\n  }\n\n  const List = {\n    template: '#re-list',\n    data() {\n      return {\n        filter: '',\n        filterPlaceholder: t('Search by title'),\n        products: [],\n        permissions: settings.permissions,\n        showActions: settings.permissions.patch && settings.permissions.delete\n      };\n    },\n    beforeRouteEnter(to, from, next) {\n      if (cachedProducts) {\n        next(vc => {vc.products = cachedProducts})\n      }\n      else {\n        productStore.getAll((products) => {\n          next(vc => {vc.products = cachedProducts = products})\n        });\n      }\n    },\n    updated() {\n      // Make sure dropbutton.js processes our buttons.\n      Drupal.attachBehaviors(document, drupalSettings);\n    },\n    computed: {\n      filteredProducts() {\n        let filter = this.filter.toLowerCase();\n        return this.products.filter(product => {\n          return !filter || product.title.toLowerCase().indexOf(filter) !== -1;\n        });\n      }\n    }\n  };\n\n  const AddForm = {\n    template: '#re-product-form',\n    data() {\n      return {\n        disabled: false,\n        product: {}\n      };\n    },\n    methods: {\n      save() {\n        if (!this.disabled) {\n          productStore.post(this.product, goToOverview);\n          this.disabled = true;\n        }\n      }\n    }\n  };\n\n  const EditForm = {\n    template: '#re-product-form',\n    data() {\n      return {\n        disabled: false,\n        product: {}\n      };\n    },\n    beforeRouteEnter(to, from, next) {\n      productStore.get(to.params.id, product => {\n        next(vc => {vc.product = product})\n      });\n    },\n    methods: {\n      save() {\n        if (!this.disabled) {\n          productStore.patch(this.product, goToOverview);\n          this.disabled = true;\n        }\n      }\n    }\n  };\n\n  const DeleteForm = {\n    template: '#re-product-delete-form',\n    methods: {\n      remove() {\n        productStore.delete(this.$route.params.id, goToOverview);\n      }\n    }\n  };\n\n  const routes = [\n    {name: 'overview', path: '/', component: List},\n    {name: 'add', path: '/product/add', component: AddForm},\n    {name: 'edit', path: '/product/:id', component: EditForm},\n    {name: 'delete', path: '/product/:id/delete', component: DeleteForm}\n  ];\n\n  /* global VueRouter */\n  const router = new VueRouter({routes: routes});\n\n  new Vue({router: router}).$mount('#re-app');\n\n  /* global RestExampleProductStore */\n}(drupalSettings.restExample, Drupal.t, RestExampleProductStore));\n\n// Translatable stings.\n/*\n Drupal.t('Add product');\n Drupal.t('Select by title');\n Drupal.t('ID');\n Drupal.t('Title');\n Drupal.t('Available');\n Drupal.t('Price');\n Drupal.t('Actions');\n Drupal.t('Yes');\n Drupal.t('No');\n Drupal.t('List additional actions');\n Drupal.t('Edit');\n Drupal.t('Delete');\n Drupal.t('No products were found.');\n Drupal.t('Description');\n Drupal.t('Save');\n Drupal.t('Cancel');\n Drupal.t('Are you sure you want to delete this product?')\n Drupal.t('This action cannot be undone.');\n*/\n"]}
\ No newline at end of file
diff --git a/rest_example/js/compile.es6.js b/rest_example/js/compile.es6.js
new file mode 100644
index 0000000..49de93b
--- /dev/null
+++ b/rest_example/js/compile.es6.js
@@ -0,0 +1,46 @@
+/**
+ * @file
+ * Watch changes to *.es6.js files and compile them to ES5 during development.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const babel = require('babel-core');
+const chokidar = require('chokidar');
+
+// Logging human-readable timestamp.
+const log = function log(message) {
+  console.log(`[${new Date().toTimeString().slice(0, 8)}] ${message}`);
+};
+
+function addSourceMappingUrl(code, loc) {
+  return `${code}\n\n//# sourceMappingURL=${path.basename(loc)}`;
+}
+
+const fileMatch = '*.es6.js';
+const watcher = chokidar.watch(fileMatch, {
+  ignoreInitial: true,
+  ignored: ['node_modules/**', 'compile.es6.js']
+});
+
+const compile = (filePath) => {
+  babel.transformFile(filePath, {
+    sourceMaps: true,
+    comments: false
+  }, (error, result) => {
+    const fileName = filePath.slice(0, -7);
+    // we've requested for a sourcemap to be written to disk
+    const mapLoc = `${fileName}.js.map`;
+
+    fs.writeFileSync(mapLoc, JSON.stringify(result.map));
+    fs.writeFileSync(`${fileName}.js`, addSourceMappingUrl(result.code, mapLoc));
+
+    log(`'${filePath}' has been changed.`);
+  });
+};
+
+watcher
+  .on('change', filePath => compile(filePath))
+  .on('ready', () => log(`Watching '${fileMatch}' for changes.`));
diff --git a/rest_example/js/package.json b/rest_example/js/package.json
new file mode 100644
index 0000000..617fd93
--- /dev/null
+++ b/rest_example/js/package.json
@@ -0,0 +1,19 @@
+{
+  "name": "rest_example",
+  "license": "GPL-2.0",
+  "scripts": {
+    "watch:js": "node ./compile.es6.js"
+  },
+  "devDependencies": {
+    "babel-cli": "^6.18.0",
+    "babel-core": "6.17.0",
+    "babel-preset-es2015": "6.16.0",
+    "chokidar": "1.6.0",
+    "glob": "^7.1.1"
+  },
+  "babel": {
+    "presets": [
+      "es2015"
+    ]
+  }
+}
diff --git a/rest_example/js/store.es6.js b/rest_example/js/store.es6.js
new file mode 100644
index 0000000..1369960
--- /dev/null
+++ b/rest_example/js/store.es6.js
@@ -0,0 +1,62 @@
+/**
+ * @file
+ * Rest example product store.
+ */
+
+window.RestExampleProductStore = class {
+
+  constructor(token) {
+    this.endPoint = 'api/rest-example/product';
+    this.token = token;
+    this.options = {
+      headers: {'X-CSRF-Token': this.token}
+    };
+  }
+
+  errorHandler(response) {
+    let message = response ?
+    'Error: ' + response.status + ' ' + response.statusText : 'Unknown message';
+    alert(message);
+  }
+
+  getAll(callback) {
+    let url = Drupal.url(this.endPoint);
+    Vue.http.get(url).then(
+      response => {response.json().then(callback)},
+      this.errorHandler
+    );
+  }
+
+  get(id, callback) {
+    let url = Drupal.url(this.endPoint + '/' + id) + '?_format=json';
+    Vue.http.get(url).then(
+      response => {response.json().then(callback)},
+      this.errorHandler
+    );
+  }
+
+  patch(product, callback) {
+    let url = Drupal.url(this.endPoint + '/' + product.id);
+    Vue.http.patch(url, product, this.options).then(
+      response => {response.json().then(callback)},
+      this.errorHandler
+    );
+  }
+
+  post(product, callback) {
+    let url = Drupal.url(this.endPoint);
+    Vue.http.post(url, product, this.options).then(
+      response => {response.json().then(callback)},
+      this.errorHandler
+    );
+  }
+
+  delete(id, callback) {
+    let url = Drupal.url(this.endPoint + '/' + id);
+    Vue.http.delete(url, this.options).then(
+      callback,
+      this.errorHandler
+    );
+  }
+
+};
diff --git a/rest_example/js/store.js b/rest_example/js/store.js
new file mode 100644
index 0000000..0945c98
--- /dev/null
+++ b/rest_example/js/store.js
@@ -0,0 +1,72 @@
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+/**
+ * @file
+ * Rest example product store.
+ */
+
+window.RestExampleProductStore = function () {
+  function _class(token) {
+    _classCallCheck(this, _class);
+
+    this.endPoint = 'api/rest-example/product';
+    this.token = token;
+    this.options = {
+      headers: { 'X-CSRF-Token': this.token }
+    };
+  }
+
+  _createClass(_class, [{
+    key: 'errorHandler',
+    value: function errorHandler(response) {
+      var message = response ? 'Error: ' + response.status + ' ' + response.statusText : 'Unknown message';
+      alert(message);
+    }
+  }, {
+    key: 'getAll',
+    value: function getAll(callback) {
+      var url = Drupal.url(this.endPoint);
+      Vue.http.get(url).then(function (response) {
+        response.json().then(callback);
+      }, this.errorHandler);
+    }
+  }, {
+    key: 'get',
+    value: function get(id, callback) {
+      var url = Drupal.url(this.endPoint + '/' + id) + '?_format=json';
+      Vue.http.get(url).then(function (response) {
+        response.json().then(callback);
+      }, this.errorHandler);
+    }
+  }, {
+    key: 'patch',
+    value: function patch(product, callback) {
+      var url = Drupal.url(this.endPoint + '/' + product.id);
+      Vue.http.patch(url, product, this.options).then(function (response) {
+        response.json().then(callback);
+      }, this.errorHandler);
+    }
+  }, {
+    key: 'post',
+    value: function post(product, callback) {
+      var url = Drupal.url(this.endPoint);
+      Vue.http.post(url, product, this.options).then(function (response) {
+        response.json().then(callback);
+      }, this.errorHandler);
+    }
+  }, {
+    key: 'delete',
+    value: function _delete(id, callback) {
+      var url = Drupal.url(this.endPoint + '/' + id);
+      Vue.http.delete(url, this.options).then(callback, this.errorHandler);
+    }
+  }]);
+
+  return _class;
+}();
+
+//# sourceMappingURL=store.js.map
\ No newline at end of file
diff --git a/rest_example/js/store.js.map b/rest_example/js/store.js.map
new file mode 100644
index 0000000..8fe3f38
--- /dev/null
+++ b/rest_example/js/store.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["store.es6.js"],"names":["window","RestExampleProductStore","token","endPoint","options","headers","response","message","status","statusText","alert","callback","url","Drupal","Vue","http","get","then","json","errorHandler","id","product","patch","post","delete"],"mappings":";;;;;;AAAA;;;;;AAKAA,OAAOC,uBAAP;AAEE,kBAAYC,KAAZ,EAAmB;AAAA;;AACjB,SAAKC,QAAL,GAAgB,0BAAhB;AACA,SAAKD,KAAL,GAAaA,KAAb;AACA,SAAKE,OAAL,GAAe;AACbC,eAAS,EAAC,gBAAgB,KAAKH,KAAtB;AADI,KAAf;AAGD;;AARH;AAAA;AAAA,iCAUeI,QAVf,EAUyB;AACrB,UAAIC,UAAUD,WACd,YAAYA,SAASE,MAArB,GAA8B,GAA9B,GAAoCF,SAASG,UAD/B,GAC4C,iBAD1D;AAEAC,YAAMH,OAAN;AACD;AAdH;AAAA;AAAA,2BAgBSI,QAhBT,EAgBmB;AACf,UAAIC,MAAMC,OAAOD,GAAP,CAAW,KAAKT,QAAhB,CAAV;AACAW,UAAIC,IAAJ,CAASC,GAAT,CAAaJ,GAAb,EAAkBK,IAAlB,CACE,oBAAY;AAACX,iBAASY,IAAT,GAAgBD,IAAhB,CAAqBN,QAArB;AAA+B,OAD9C,EAEE,KAAKQ,YAFP;AAID;AAtBH;AAAA;AAAA,wBAwBMC,EAxBN,EAwBUT,QAxBV,EAwBoB;AAChB,UAAIC,MAAMC,OAAOD,GAAP,CAAW,KAAKT,QAAL,GAAgB,GAAhB,GAAsBiB,EAAjC,IAAuC,eAAjD;AACAN,UAAIC,IAAJ,CAASC,GAAT,CAAaJ,GAAb,EAAkBK,IAAlB,CACE,oBAAY;AAACX,iBAASY,IAAT,GAAgBD,IAAhB,CAAqBN,QAArB;AAA+B,OAD9C,EAEE,KAAKQ,YAFP;AAID;AA9BH;AAAA;AAAA,0BAgCQE,OAhCR,EAgCiBV,QAhCjB,EAgC2B;AACvB,UAAIC,MAAMC,OAAOD,GAAP,CAAW,KAAKT,QAAL,GAAgB,GAAhB,GAAsBkB,QAAQD,EAAzC,CAAV;AACAN,UAAIC,IAAJ,CAASO,KAAT,CAAeV,GAAf,EAAoBS,OAApB,EAA6B,KAAKjB,OAAlC,EAA2Ca,IAA3C,CACE,oBAAY;AAACX,iBAASY,IAAT,GAAgBD,IAAhB,CAAqBN,QAArB;AAA+B,OAD9C,EAEE,KAAKQ,YAFP;AAID;AAtCH;AAAA;AAAA,yBAwCOE,OAxCP,EAwCgBV,QAxChB,EAwC0B;AACtB,UAAIC,MAAMC,OAAOD,GAAP,CAAW,KAAKT,QAAhB,CAAV;AACAW,UAAIC,IAAJ,CAASQ,IAAT,CAAcX,GAAd,EAAmBS,OAAnB,EAA4B,KAAKjB,OAAjC,EAA0Ca,IAA1C,CACE,oBAAY;AAACX,iBAASY,IAAT,GAAgBD,IAAhB,CAAqBN,QAArB;AAA+B,OAD9C,EAEE,KAAKQ,YAFP;AAID;AA9CH;AAAA;AAAA,4BAgDSC,EAhDT,EAgDaT,QAhDb,EAgDuB;AACnB,UAAIC,MAAMC,OAAOD,GAAP,CAAW,KAAKT,QAAL,GAAgB,GAAhB,GAAsBiB,EAAjC,CAAV;AACAN,UAAIC,IAAJ,CAASS,MAAT,CAAgBZ,GAAhB,EAAqB,KAAKR,OAA1B,EAAmCa,IAAnC,CACEN,QADF,EAEE,KAAKQ,YAFP;AAID;AAtDH;;AAAA;AAAA","file":"store.es6.js","sourcesContent":["/**\n * @file\n * Rest example product store.\n */\n\nwindow.RestExampleProductStore = class {\n\n  constructor(token) {\n    this.endPoint = 'api/rest-example/product';\n    this.token = token;\n    this.options = {\n      headers: {'X-CSRF-Token': this.token}\n    };\n  }\n\n  errorHandler(response) {\n    let message = response ?\n    'Error: ' + response.status + ' ' + response.statusText : 'Unknown message';\n    alert(message);\n  }\n\n  getAll(callback) {\n    let url = Drupal.url(this.endPoint);\n    Vue.http.get(url).then(\n      response => {response.json().then(callback)},\n      this.errorHandler\n    );\n  }\n\n  get(id, callback) {\n    let url = Drupal.url(this.endPoint + '/' + id) + '?_format=json';\n    Vue.http.get(url).then(\n      response => {response.json().then(callback)},\n      this.errorHandler\n    );\n  }\n\n  patch(product, callback) {\n    let url = Drupal.url(this.endPoint + '/' + product.id);\n    Vue.http.patch(url, product, this.options).then(\n      response => {response.json().then(callback)},\n      this.errorHandler\n    );\n  }\n\n  post(product, callback) {\n    let url = Drupal.url(this.endPoint);\n    Vue.http.post(url, product, this.options).then(\n      response => {response.json().then(callback)},\n      this.errorHandler\n    );\n  }\n\n  delete(id, callback) {\n    let url = Drupal.url(this.endPoint + '/' + id);\n    Vue.http.delete(url, this.options).then(\n      callback,\n      this.errorHandler\n    );\n  }\n\n};\n"]}
\ No newline at end of file
diff --git a/rest_example/rest_example.info.yml b/rest_example/rest_example.info.yml
new file mode 100644
index 0000000..bb8cceb
--- /dev/null
+++ b/rest_example/rest_example.info.yml
@@ -0,0 +1,8 @@
+name: Rest example
+type: module
+description: Example of REST application.
+package: Example modules
+core: 8.x
+dependencies:
+  - examples
+  - rest
diff --git a/rest_example/rest_example.install b/rest_example/rest_example.install
new file mode 100644
index 0000000..98e0923
--- /dev/null
+++ b/rest_example/rest_example.install
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Rest example module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function rest_example_schema() {
+
+  $schema['rest_example_product'] = [
+    'description' => 'Example products.',
+    'fields' => [
+      'id' => [
+        'description' => 'The primary identifier for a product.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ],
+      'title' => [
+        'description' => 'The product title.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ],
+      'status' => [
+        'description' => 'Boolean indicating whether the product is available.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+      ],
+      'description' => [
+        'type' => 'text',
+        'size' => 'big',
+        'description' => 'Product description.',
+      ],
+      'price' => [
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'description' => 'Product price.',
+      ],
+    ],
+
+    'primary key' => ['id'],
+  ];
+
+  return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function rest_example_install() {
+  // Create some demo products.
+  $records[] = [
+    'title' => 'Alpha',
+    'status' => 1,
+    'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent ante sem, venenatis sed sapien eu, elementum viverra sapien. Quisque non arcu aliquam, luctus orci eget, condimentum velit. Nunc dapibus, est non vestibulum elementum, eros felis accumsan ligula, vel accumsan leo sapien sit amet tortor.',
+    'price' => 100,
+  ];
+
+  $records[] = [
+    'title' => 'Beta',
+    'status' => 1,
+    'description' => 'Nulla varius facilisis ipsum sed pulvinar. Donec eu odio orci. Suspendisse dapibus nibh vitae dolor sodales, a porttitor lacus hendrerit.',
+    'price' => 200,
+  ];
+
+  $records[] = [
+    'title' => 'Gamma',
+    'status' => 1,
+    'description' => 'Aliquam pulvinar odio vitae nisl lobortis, quis imperdiet nisl vehicula. Integer vestibulum elit commodo, pellentesque ante sed, finibus neque. Donec non fringilla est. Nullam ex eros, convallis nec convallis eu, commodo sed dui. Mauris quis interdum tellus.',
+    'price' => 300,
+  ];
+
+  $query = Drupal::database()
+    ->insert('rest_example_product')
+    ->fields(array_keys($records[0]));
+  foreach ($records as $record) {
+    $query->values($record);
+  }
+  $query->execute();
+}
diff --git a/rest_example/rest_example.libraries.yml b/rest_example/rest_example.libraries.yml
new file mode 100644
index 0000000..3f1aa70
--- /dev/null
+++ b/rest_example/rest_example.libraries.yml
@@ -0,0 +1,47 @@
+vuejs:
+  remote: https://vuejs.org
+  version: 2.0.3
+  license:
+    name: MIT
+    url: https://github.com/vuejs/vue/blob/dev/LICENSE
+    gpl-compatible: true
+  js:
+    https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.7/vue.min.js: {type: external, minified: true}
+
+vuejs-router:
+  remote: http://router.vuejs.org
+  version: 2.0.1
+  license:
+    name: MIT
+    url: https://github.com/vuejs/vue-router/blob/dev/LICENSE
+    gpl-compatible: true
+  js:
+    https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.0.2/vue-router.min.js: {type: external, minified: true}
+  dependencies:
+    - rest_example/vue.js
+
+vuejs-resource:
+  remote: https://github.com/vuejs/vue-resource
+  version: 1.0.3
+  license:
+    name: MIT
+    url: https://github.com/vuejs/vue-resource/blob/master/LICENSE
+    gpl-compatible: true
+  js:
+    https://cdnjs.cloudflare.com/ajax/libs/vue-resource/1.0.3/vue-resource.min.js: {type: external, minified: true}
+  dependencies:
+    - rest_example/vue.js
+
+app:
+  js:
+    js/store.js: {}
+    js/app.js: {}
+  css:
+    component:
+     css/rest-example.css: {}
+  dependencies:
+    - core/drupalSettings
+    - core/drupal.dropbutton
+    - rest_example/vuejs
+    - rest_example/vuejs-router
+    - rest_example/vuejs-resource
diff --git a/rest_example/rest_example.links.menu.yml b/rest_example/rest_example.links.menu.yml
new file mode 100644
index 0000000..4314b96
--- /dev/null
+++ b/rest_example/rest_example.links.menu.yml
@@ -0,0 +1,3 @@
+rest_example.rest_application:
+  title: Rest application
+  route_name: rest_example.rest_application
diff --git a/rest_example/rest_example.module b/rest_example/rest_example.module
new file mode 100644
index 0000000..9703090
--- /dev/null
+++ b/rest_example/rest_example.module
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Primary module hooks for Rest example module.
+ */
+
+/**
+ * Implements hook_theme().
+ */
+function rest_example_theme() {
+  return ['rest_example_app' => ['variables' => []]];
+}
diff --git a/rest_example/rest_example.routing.yml b/rest_example/rest_example.routing.yml
new file mode 100644
index 0000000..6592b77
--- /dev/null
+++ b/rest_example/rest_example.routing.yml
@@ -0,0 +1,16 @@
+rest_example.rest_application:
+  path: '/examples/rest-application'
+  defaults:
+    _controller: '\Drupal\rest_example\Controller\RestExampleController::build'
+    _title: 'REST application'
+  requirements:
+    _permission: 'restful get rest_example_product'
+
+rest_example.product_collection_export:
+  path: '/api/rest-example/product'
+  defaults:
+    _controller: '\Drupal\rest_example\Controller\RestExampleController::export'
+    _title: 'REST application'
+  requirements:
+    _permission: 'restful get rest_example_product'
+
diff --git a/rest_example/src/Controller/RestExampleController.php b/rest_example/src/Controller/RestExampleController.php
new file mode 100644
index 0000000..b51ca3e
--- /dev/null
+++ b/rest_example/src/Controller/RestExampleController.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\rest_example\Controller;
+
+use Drupal\Core\Access\CsrfRequestHeaderAccessCheck;
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * Returns responses for Rest example routes.
+ */
+class RestExampleController extends ControllerBase {
+
+  /**
+   * The default database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * The CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfTokenGenerator;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * Constructs the controller object.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The default database connection.
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token_generator
+   *   The CSRF token generator.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   */
+  public function __construct(Connection $connection, CsrfTokenGenerator $csrf_token_generator, AccountInterface $account) {
+    $this->connection = $connection;
+    $this->csrfTokenGenerator = $csrf_token_generator;
+    $this->account = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('database'),
+      $container->get('csrf_token'),
+      $container->get('current_user')
+    );
+  }
+
+  /**
+   * Builds product overview page.
+   */
+  public function build() {
+    $token = $this->csrfTokenGenerator->get(CsrfRequestHeaderAccessCheck::TOKEN_KEY);
+
+    $build['content'] = ['#theme' => 'rest_example_app'];
+    $build['#attached']['library'][] = 'rest_example/app';
+    $build['#attached']['drupalSettings']['restExample']['token'] = $token;
+
+    $permissions = [
+      'post' => $this->account->hasPermission('restful post rest_example_product'),
+      'patch' => $this->account->hasPermission('restful patch rest_example_product'),
+      'delete' => $this->account->hasPermission('restful delete rest_example_product'),
+    ];
+    $build['#attached']['drupalSettings']['restExample']['permissions'] = $permissions;
+
+    if (count(array_filter($permissions)) < 3) {
+      drupal_set_message(t('You do not have sufficient permissions to manage example products.'), 'warning');
+    }
+
+    return $build;
+  }
+
+  /**
+   * Exports products.
+   */
+  public function export() {
+    $products = $this->connection->query('SELECT id, title, status, price FROM {rest_example_product} ORDER BY id')->fetchAll();
+    return new JsonResponse($products);
+  }
+
+}
diff --git a/rest_example/src/Plugin/rest/resource/ExampleResource.php b/rest_example/src/Plugin/rest/resource/ExampleResource.php
new file mode 100644
index 0000000..16cd6ea
--- /dev/null
+++ b/rest_example/src/Plugin/rest/resource/ExampleResource.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace Drupal\rest_example\Plugin\rest\resource;
+
+use Drupal\Component\Plugin\DependentPluginInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\rest\ModifiedResourceResponse;
+use Drupal\rest\Plugin\ResourceBase;
+use Drupal\rest\ResourceResponse;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Represents Example products as resources.
+ *
+ * @RestResource (
+ *   id = "rest_example_product",
+ *   label = @Translation("Example product"),
+ *   uri_paths = {
+ *     "canonical" = "/api/rest-example/product/{id}",
+ *     "https://www.drupal.org/link-relations/create" = "/api/rest-example/product"
+ *   }
+ * )
+ */
+class ExampleResource extends ResourceBase implements DependentPluginInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $dbConnection;
+
+  /**
+   * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param array $serializer_formats
+   *   The available serialization formats.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   * @param \Drupal\Core\Database\Connection $db_connection
+   *   The database connection.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, Connection $db_connection) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
+    $this->dbConnection = $db_connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->getParameter('serializer.formats'),
+      $container->get('logger.factory')->get('rest'),
+      $container->get('database')
+    );
+  }
+
+  /**
+   * Responds to GET requests.
+   *
+   * @param int $id
+   *   The ID of the product.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The response containing the product.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function get($id) {
+    $product = $this->loadProduct($id);
+    $response = new ResourceResponse($product);
+    // Make the response uncacheable.
+    $response->addCacheableDependency(new \stdClass());
+    return $response;
+  }
+
+  /**
+   * Responds to POST requests and saves the new product.
+   *
+   * @param array $product
+   *   An associative array of fields to insert into the database.
+   *
+   * @return \Drupal\rest\ModifiedResourceResponse
+   *   The HTTP response object.
+   */
+  public function post($product) {
+    $this->validate($product);
+
+    $id = $this->dbConnection->insert('rest_example_product')
+      ->fields($product)
+      ->execute();
+
+    $this->logger->notice('Product %title has been created.', ['%title' => $product['title']]);
+
+    $created_product = $this->loadProduct($id);
+
+    // Return the newly created product in the response body.
+    return new ModifiedResourceResponse($created_product, 201);
+  }
+
+  /**
+   * Responds to product PATCH requests.
+   *
+   * @param int $id
+   *   The ID of the product.
+   * @param array $product
+   *   An associative array of fields to write into the database.
+   *
+   * @return \Drupal\rest\ModifiedResourceResponse
+   *   The HTTP response object.
+   */
+  public function patch($id, $product) {
+    // Make sure the product sill exists.
+    $this->loadProduct($id);
+
+    $this->validate($product);
+
+    // Record ID should never be changed.
+    unset($product['id']);
+
+    $this->dbConnection->update('rest_example_product')
+      ->fields($product)
+      ->condition('id', $id)
+      ->execute();
+
+    $this->logger->notice('Product %title has been updated', ['%title' => $product['title']]);
+
+    // Return the updated product in the response body.
+    $updated_product = $this->loadProduct($id);
+    return new ModifiedResourceResponse($updated_product, 200);
+  }
+
+  /**
+   * Responds to product DELETE requests.
+   *
+   * @param int $id
+   *   The ID of the product.
+   *
+   * @return \Drupal\rest\ModifiedResourceResponse
+   *   The HTTP response object.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function delete($id) {
+    // Make sure the product still exists.
+    $product = $this->loadProduct($id);
+
+    $this->dbConnection->delete('rest_example_product')
+      ->condition('id', $id)
+      ->execute();
+
+    $this->logger->notice('Deleted product %title.', ['%title' => $product['title']]);
+
+    // DELETE responses have an empty body.
+    return new ModifiedResourceResponse(NULL, 204);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBaseRoute($canonical_path, $method) {
+    $route = parent::getBaseRoute($canonical_path, $method);
+
+    // Change ID validation pattern.
+    if ($method != 'POST') {
+      $route->setRequirement('id', '\d+');
+    }
+
+    return $route;
+  }
+
+  /**
+   * Validates incoming product.
+   *
+   * @param array $product
+   *   An associative array of fields to write into the database.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   */
+  protected function validate(&$product) {
+    if (!is_array($product)) {
+      throw new BadRequestHttpException('No product content received.');
+    }
+
+    $product_fields = ['id', 'title', 'status', 'description', 'price'];
+    foreach ($product as $name => $value) {
+      if (!in_array($name, $product_fields)) {
+        throw new BadRequestHttpException('Unknown product field.');
+      }
+      elseif (!is_null($value) && !is_scalar($value)) {
+        throw new BadRequestHttpException('Wrong field value.');
+      }
+    }
+
+    // The more graceful way could be returning these validation errors in a
+    // regular HTTP response and displaying them inline with form elements.
+    if (empty($product['title'])) {
+      throw new BadRequestHttpException('Title field is required.');
+    }
+    elseif (strlen($product['title']) > 255) {
+      throw new BadRequestHttpException('Title field is too big.');
+    }
+
+    if (empty($product['price'])) {
+      $product['price'] = NULL;
+    }
+    elseif (!ctype_digit((string) $product['price'])) {
+      throw new BadRequestHttpException('Price field should be positive integer.');
+    }
+    elseif ($product['price'] > 1000000) {
+      throw new BadRequestHttpException('Price field is too big.');
+    }
+
+    $product['status'] = (integer) $product['status'];
+  }
+
+  /**
+   * Loads a product record from the database.
+   *
+   * @param int $id
+   *   The ID of the product.
+   *
+   * @return array
+   *   The product data.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  protected function loadProduct($id) {
+    $product = $this->dbConnection->query('SELECT * FROM {rest_example_product} WHERE id = :id', [':id' => $id])->fetchAssoc();
+    if (!$product) {
+      throw new NotFoundHttpException('The product was not found.');
+    }
+    return $product;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+}
diff --git a/rest_example/templates/rest-example-app.html.twig b/rest_example/templates/rest-example-app.html.twig
new file mode 100644
index 0000000..f33de0b
--- /dev/null
+++ b/rest_example/templates/rest-example-app.html.twig
@@ -0,0 +1,102 @@
+{% verbatim %}
+
+<div id="re-app" v-cloak>
+  <transition name="re-transition">
+    <router-view></router-view>
+  </transition>
+</div>
+
+<template id="re-list">
+  <div>
+    <div v-if="permissions.post">
+      <router-link :to="{name: 'add'}" class="button button-action" v-t>Add product</router-link>
+    </div>
+    <div class="re-filter">
+      <div class="form-item">
+        <input type="search" id="re-title-filter" class="form-search" :placeholder="filterPlaceholder" v-model="filter"/>
+      </div>
+    </div>
+    <table>
+      <thead>
+        <tr>
+          <th v-t>ID</th>
+          <th v-t>Title</th>
+          <th v-t>Available</th>
+          <th v-t>Price</th>
+          <th v-t v-if="showActions">Actions</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="product in filteredProducts">
+          <td>{{ product.id }}</td>
+          <td>{{ product.title }}</td>
+          <td>{{ product.status == 1 ? 'Yes' : 'No' }}</td>
+          <td>{{ product.price }}</td>
+          <td class="re-actions-column" v-if="showActions">
+            <div class="dropbutton-wrapper dropbutton-multiple">
+              <div class="dropbutton-widget">
+                <ul class="dropbutton">
+                  <li class="manage-fields dropbutton-action">
+                    <router-link :to="{name: 'edit', params: {id: product.id}}" v-t>Edit</router-link>
+                  </li>
+                  <li class="dropbutton-toggle">
+                    <button type="button">
+                      <span class="dropbutton-arrow">
+                        <span class="visually-hidden" v-t>List additional actions</span>
+                      </span>
+                    </button></li>
+                  <li class="manage-form-display dropbutton-action secondary-action">
+                    <router-link :to="{name: 'delete', params: {id: product.id}}" v-t>Delete</router-link>
+                  </li>
+                </ul>
+              </div>
+            </div>
+          </td>
+        </tr>
+        <tr v-show="filteredProducts.length == 0">
+          <td :colspan="showActions ? 5 : 4" v-t>No products were found.</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+
+<template id="re-product-form">
+  <form @submit="save()">
+    <div class="form-item form-type-textfield">
+      <label for="re-form-title" v-t>Title</label>
+      <input type="text" id="re-form-title" class="form-text" required v-model="product.title"/>
+    </div>
+    <div class="form-item form-type-checkbox">
+      <input type="checkbox" id="re-form-status" class="form-checkbox" v-model="product.status"/>
+      <label for="re-form-status" class="option" v-t>Available</label>
+    </div>
+    <div class="form-item form-textarea-wrapper">
+      <label for="re-form-description" v-t>Description</label>
+      <textarea id="re-form-description" class="form-textarea" rows="5" v-model="product.description"></textarea>
+    </div>
+    <div class="form-item form-type-number">
+      <label for="re-form-price" v-t>Price</label>
+      <input type="number" min="1" id="re-form-price" class="form-number" v-model="product.price"/>
+    </div>
+    <div class="form-actions">
+      <button type="submit" class="button button--primary form-submit" v-t>Save</button>
+      <router-link :to="{name: 'overview'}" class="button" v-t>Cancel</router-link>
+    </div>
+  </form>
+</template>
+
+<template id="re-product-delete-form">
+  <div>
+    <div class="form-item ">
+      <h2 v-t>Are you sure you want to delete this product?</h2>
+      <div for="re-form-title" v-t>This action cannot be undone.</div>
+    </div>
+    <div class="form-actions">
+      <button  type="submit" class="button button--primary form-submit" @click="remove()" v-t>Delete</button>
+      <router-link :to="{name: 'overview'}" class="button" v-t>Cancel</router-link>
+    </div>
+  </div>
+</template>
+
+{% endverbatim %}
