Decoupled Drupal with JSON API and Ember: Consuming Drupal with Ember Adapters and Models

  • 12 minute read

Among the most crucial steps in architecting decoupled Drupal-backed applications is to bridge the gap between Drupal and the designated front end so that the latter can receive and manipulate data on the Drupal data repository via API calls. For some frameworks, this can be a rather tedious exercise in navigating the server-side APIs and crafting the correct requests on the client side. Luckily, with JSON API now proposed as a core experimental module for Drupal 8, the tightrope walk between Drupal and Ember is about to become more of a cinch.

In the first installment of this tutorial series, we installed Drupal and configured it as a data service that exposes content entities through JSON API. We also installed and set up Ember and examined some of its key components up close, including the Ember software stack, templates, routes, and components. In this second part, we’ll dive into how to use Ember Data’s persistence layer and the model hook, what Ember adapters are and how they work, and how to drill down into Drupal-provided properties after requesting our needed data. Lastly, we’ll zoom out to the bigger picture and consider the implications of this approach on the future of Drupal.

Ember models

This section resumes from the final example in the previous part of this tutorial series. Ember models represent persistent state on the client side and typically persist data to a web server, though they can save to anywhere else. When data changes, or new data is added, the model is saved. In Ember, models are generated similarly to routes and components.

To proceed, generate a model for articles, pages, and users as follows. Recall that g is a shorthand for the Ember command generate.

$ ember g model node--article
$ ember g model node--page
$ ember g model user--user

As you may have noticed, the model names for articles, pages, and users differ from those conferred to the corresponding routes. If you navigate to /api/node/article?_format=api_json and scrutinize the first available property, named type, you’ll see that Drupal’s way of recording resource object types in JSON API follows the pattern we provided as arguments above.

With the generated models now in place, we can identify the properties we wish to have available while developing our application by using the .attr() method, provided by Ember Data, within each model’s JavaScript file. Under the attributes property in the JSON API feed, you can see the properties that are available for each content entity, including uuid and others.

// app/models/node--article.js
import DS from 'ember-data';

export default DS.Model.extend({
  nid: DS.attr(),
  uuid: DS.attr(),
  title: DS.attr(),
  created: DS.attr(),
  body: DS.attr()
});

// app/models/node--page.js
import DS from 'ember-data';

export default DS.Model.extend({
  nid: DS.attr(),
  uuid: DS.attr(),
  title: DS.attr(),
  created: DS.attr(),
  body: DS.attr()
});

// app/models/user--user.js
import DS from 'ember-data';

export default DS.Model.extend({
  uid: DS.attr(),
  uuid: DS.attr(),
  name: DS.attr(),
  mail: DS.attr()
});

There are several things to note here which may be of interest to developers with particular requirements for their models. First, the .attr() method is often invoked to cast inputs to a different type, such as transforming an integer into a string (.attr('string')). Second, there is no further drilling needed for the body attribute, because Ember Data will intelligently capture all of the child properties. Therefore we don’t need to add a separate property for the body value.

Ember adapters and connecting to Drupal

There is only one absent piece in order for our application to work correctly: the connection to Drupal. Ember uses what are known as adapters to communicate with back ends via XMLHttpRequests. In the example below, we create an adapter which straddles our entire application, because we’ll be ingesting data from only a single Drupal site. However, you can provide an arbitrary argument when generating an adapter if you need to connect to multiple data sources across different points in your application.

$ ember generate adapter application

By default, Ember will generate a JSONAPIAdapter, its default adapter, for use with JSON API back ends, and we can customize these adapters according to the corresponding back end. Ember Data has other adapters available for REST APIs that don’t adhere to the JSON API specification, though these typically require additional steps to set up. To connect to the Drupal repository we have available, provide a host and namespace, as follows.

// app/adapters/application.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  host: 'http://waterfire-drupal.dd:8083',
  namespace: 'api'
});

Fetching data in route handlers

Within our route handlers, where we had provisioned dummy data, we can now employ our Drupal data store by updating each model hook. The following will fetch the needed data from the Drupal site and automatically import it into the model for use by the application.

// app/routes/articles.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.get('store').findAll('node--article');
  }
});
// app/routes/pages.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.get('store').findAll('node--page');
  }
});
// app/routes/users.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.get('store').findAll('user--user');
  }
});

If you try to navigate to the routes in the application as we’ve allocated them, you’ll notice that our app is not functional. With Ember Inspector, you’ll discover that the models are not being populated in the first place, meaning that our issue is on the back end. If you take a look at the Drupal error log (/admin/reports/dblog), 404s will appear that indicate the fetches are being issued against incorrect paths such as /api/node--articles, etc.

Customizing the JSON API adapter

In order to let the JSONAPIAdapter recognize our Drupal site correctly, we need to customize our adapter further by providing code permitting it to recognize the correct paths in the Drupal-provided REST API. The following code translates the input path (e.g. node--article) into the appropriate one available in the REST API (e.g. node/article). Special thanks to Chris Hamper for his optimization of this code.

// app/adapters/application.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  host: 'http://dublin-drupal.dd:8083',
  namespace: 'api',

  pathForType(type) {
    let entityPath;
    switch(type) {
      case 'node--article':
        entityPath = 'node/article';
        break;
      case 'node--page':
        entityPath = 'node/page';
        break;
      case 'user--user':
        entityPath = 'user/user';
        break;
    }
    return entityPath;
  },

  buildURL() {
    return this._super(...arguments) + '?_format=api_json';
  }
});

There are several syntactic features from ES6 which are of particular interest here, besides the concise method naming we already highlighted in the previous installment of this series. First, the let keyword is an alternative to the ES5 keyword var. Both define variables, but whereas var is scoped to the surrounding function, which can cause scope issues, let is scoped to the surrounding block bounded by curly braces. Second, while the spread operator has many uses, in this situation it allows the ._super() method to be provided arguments without regard to their quantity.

Accessing properties in templates

Now, we can incorporate Drupal data into each route template by replacing the components we referred to earlier with code which iterates over the provisioned model and accesses its properties in turn:

{{! app/templates/articles.hbs }}

List of articles

{{#each model as |article|}}

{{article.title}}

  • NID: {{article.nid}}
  • UUID: {{article.uuid}}
  • Created: {{article.created}}

{{article.body.value}} {{/each}}


{{! app/templates/pages.hbs }}

List of pages

{{#each model as |page|}}

{{page.title}}

  • NID: {{page.nid}}
  • UUID: {{page.uuid}}
  • Created: {{page.created}}

{{page.body.value}} {{/each}}


{{! app/templates/users.hbs }}

List of users

{{#each model as |user|}}

{{user.name}}

  • UID: {{user.uid}}
  • UUID: {{user.uuid}}
  • E-mail: {{user.mail}}

{{/each}}

That’s it! The code we’ve written up to this point is currently available on GitHub if you’d like to compare the result. By clicking around the application, you can see that routes update dynamically and Ember Data retains some information client-side to avoid server requests. Moreover, developers more accustomed to an MV* paradigm or to modern JavaScript practices will note that syntactic features as yet unavailable (but provisionally forthcoming) in Drupal are conveniently available with minimal friction thanks to Ember CLI.

If you’re looking for some next steps to challenge yourself further, here are a few tasks to advance your knowledge:

  1. We discarded the entity-list component which we created in the first part of this tutorial. Can you create a new entity-list component by generalizing the route templates we have just modified?
  2. We have created nothing more than a simple content browser which performs GET queries. Can you extend the application to perform create, update, and delete queries as well?

Conclusion

Over the course of this tutorial series, we’ve undertaken the construction of a Drupal-backed Ember application from start to finish, from provisioning Drupal’s data by means of JSON API to leveraging Ember’s systems for a graceful — and relatively easily implemented — result. In the first part, we configured Drupal as a series of API endpoints exporting our content entities to the front end and juggled Ember templates, routes, and components to build a cohesive application.

In the latter half of this series, we employed Ember Data’s persistence layer and Ember adapters to connect Drupal to Ember. Because Ember’s default JSON API adapter was a bit too presumptuous, we then customized it to acknowledge some of the nuances of Drupal’s JSON API implementation. Finally, we burrowed into the available properties to yield the appropriate data for our application.

Epilogue: Ember + Drupal = Drupal?

This tutorial series demonstrates that decoupled Drupal and an Ember front end can coexist and yield significant benefits for an improved user experience and front-end developer experience. Now that we’ve considered the more procedural steps, what about the bigger picture?

Several months ago, Dries Buytaert authored a blog post entitled “Can Drupal outdo native applications?” introducing the notion of inversion of control, whereby state changes formerly initiated on the server side are handed off to the client side instead. The post featured Ed Faulkner’s enthusiastically received session at DrupalCon New Orleans, “Amazing user experiences with Drupal and Ember,” which demonstrated that inversion of control can easily be applied to Drupal by utilizing Ember as an enhanced front end.

Decoupled Drupal may or may not be the future of Drupal out of the box. Nevertheless, if future versions of Drupal reflect the continuing proliferation of decoupled architectures by prestiging an API-first approach, they can still provide the end-to-end contiguity that has always characterized Drupal. For instance, if Drupal were to adopt Ember or another framework as a canonical front end in a decoupled setting, that alone would not weaken Drupal’s identity, so long as the cohesive user experience Drupal end users have come to expect remains a high priority.

It is my personal hope that by learning about and interacting with other technologies around us, we can continue to craft and map out a promising future for Drupal, without jettisoning what elevated Drupal in our eyes in the first place.

This is a two-part series adapted from the DrupalCon Dublin session “Decoupled Drupal and Ember.” The first part is available here.