apike.ca

Headless Drupal: RESTful Web Services API

By mbystedt on Jun 22, 2019, 9:06:23 PM

Once upon a time, I built my personal websites using my own script written in Perl. I later moved on to using Drupal. I'm happy enough using Drupal for content editing and administration. I found two main pain points. First, I'm very weary of the stream of updates and security patches that a relatively small number of installed modules can generate. Second, adding functionality and interactivity to the pages are processes lensed through Drupal's PHP and JavaScript APIs. This is a blessing and a curse. Drupal has done a great job of building a large community of plug and play functionality that plays well with other code written for it. The curse part is that each major Drupal release freezes most innovation in order for there to be stable platform for the module community to build upon.

One area of innovation in web design has been single page applications (SPA). A JavaScript library like Angular, Vue or React can retrieve data from an APIs to display webpages. The single page application architecture moves most presentation to the browser. So, a single page application just needs a headless interface to the data it displays.

With Drupal 8, I wanted to investigate if its RESTful Web Services API makes headless Drupal a reality. Ideally, I also wanted to ditch all my Drupal modules on the front-end. Each single page application JavaScript library has their own large community of plug and play functionality so it seemed plausible.

What was the result of my testing? There are some shortcomings, but headless Drupal is now a reality.

Drupal 8's RESTful Web Services API

The RESTful Web Services API provides a number of REST resources for built-in objects like users, nodes and others. Drupal's paths have always been REST-like with singular collection nouns such as 'user' in the path. The API leverages these paths rather than inventing new ones. Essentially, most of the REST resource urls will render a webpage if entered into a browser. If a format parameter is included in a GET request, Drupal outputs the entity in the desired format instead of as a webpage.

/drupal/user/1
Url to generate a html webpage for a user
/drupal/user/1?_format=json
Url to generate JSON output for a user
{
  "uid": [{ "value": 1 }],
  "uuid": [{ "value": "b3efc0ff-e8d0-4749-b2ec-87db154cbf2c" }],
  "langcode": [{ "value": "en" }],
  "name": [{ "value": "mbystedt" }],
  "created": [{
    "value": "2012-11-05T03:53:14+00:00",
    "format": "Y-m-d\\TH:i:sP"
  }],
  "changed": [{
    "value": "2018-08-06T21:51:04+00:00",
    "format": "Y-m-d\\TH:i:sP"
  }],
  "default_langcode": [{ "value": true }],
  "field_real_name": [{ "value": "Matthew Bystedt" }],
  "user_picture": []
}
Code 1. JSON output for a user

The configuration for RESTful Web Services is done through the REST UI module or using yml files. The later is not well documented. The configuration allows you to enable supported REST verbs (GET, POST, etc.), determine which formats (JSON, HAL, etc.) can be requested and enable authentication mechanisms.

REST resource requests will also be rejected if they do not conform to the permissions setup in Drupal.

Steps to enable

These step assume a default setup.

  1. Open Drupal admin to Extend tab
  2. Enable 'RESTful Web Services' module (Requires 'Serialization' module)
  3. Install and enable 'REST UI' module (optional)
  4. Configure 'REST UI' or yml files and set permissions (if necessary)

Limitations

For better or worse, the output data is a conversion of the PHP object that Drupal uses. This means there's arrays and value keys in the data that are probably unnecessary. Creating a view that outputs JSON is one way to output more compact data.

Drupal's permissions are by no means API-first. It isn't completely spelled out how granting or removing a permission changes access to a REST resource. Granularity that could be useful is sometimes missing. For example, you can currently only grant 'administration' on the views module. There's no permission to just view the views.

A limitation of the built-in REST resources is that they don't provide a way to read most collections. You can get a user but you can't use it to list users. Fortunately, the Views module is quite capable of outputting collections.

Views as Rest Collection APIs

Drupal 8 doesn't provide out of the box REST collection APIs. The Views module in Drupal and its JSON output option mostly handles this. The Views module is a robust solution for generating collections of things stored in Drupal's database, but the built-in JSON serializer is too basic for most uses. It doesn't include pagination details like the total number of results.

Fortunately, Stack Exchange has a good response to the question of how to create a module that adds a View serializer for pagination. I shamelessly based mine off this code and you can, too!

{
  "results": [
    {
      "body": "<p>Why are some websites faster than others? For this first article, we look at what goes into moving data over the internet in a consistent, fast and responsive manner.<\/p>",
      "field_image": "",
      "field_tags": "Programming",
      "title": "Website Performance: Part 1 - Networks",
      "view_node": "\/drupal\/content\/2019\/03\/slow-websites-networks.html",
      "uid": "mbystedt",
      "type": "Article",
      "created": "March 28, 2019",
      "nid": "151",
      "status": "On"
    },
// ...
  ],
  "pager": {
    "current_page": 0,
    "items_per_page": 10,
    "total_items": "34"
  }
}
Code 2. JSON output for the front page view collection

Building a Single Page Application

I built the Single Page Application that called Drupal's REST APIs in Angular (v8). The code is up on GitHub. My main website made for a fairly straightforward test project. It determines the desired page to load in the router, requests the page from the Drupal API and displays the content. If the page doesn't exist then you get a "page not found" error. It intercepts clicks on links and sends the navigation through the Angular router so that they didn't trigger a full page reload.

The main conflict I had with Drupal had to do with paths. Drupal doesn't understand the front-end is running inside of a single page application on a different path. So, the code needed to correct the path of a number of links.

I wanted to insert dynamic components into the raw page text. So, I originally parsed the page text for flags (basically custom html tags) that instantiated the desired component. The "left over" text blocks were instantiated into raw text components. This created an array components that made up a page.

I saw a presentation on Angular Elements and I completely abandoned the previous approach. It worked well, but the ability to define new HTML elements greatly simplified things. Custom elements are a powerful tool that I look forward to using more in the future.

Headless Drupal Conclusions

I'ld call the single page application conversion of my website a success. It is noticeably faster than having Drupal output the front-end. Navigating from page to page is snappy. I can easily extend and add new interactive elements without needing to mess around with custom Drupal modules. I also get bugged considerably less to update modules because the Drupal site has a single third party module installed.

It is not a solution for everyone, though. I threw out the baby with the bath water. Drupal 8's BigPipe Module offers much the same speed improvements as a SPA. A more practical approach would have been to use BigPipe instead of creating a custom SPA. I could still have reduced the third party module count by using custom elements that called REST APIs. It would have been less efficient, but it would have played nicely with the Drupal platform.

Drupal 8.7.x added JSON:API to the core which also could change my approach in the future.

Epilogue: Decoupled Drupal

Drupal founder and lead developer, Dries Buytaert, has written an informative post on how to decouple Drupal. The headless approach in this post is what could be called fully "Decoupled Drupal".