The Drupal 8 REST module provides a useful framework that allows you to quickly set up REST resources for your Drupal CMS. As decoupled approaches like mobile and Javascript apps continue to rise in popularity, it will become increasingly important for both backend and frontend developers to gain familiarity with the Drupal REST module and some of its quirks.

Right now there are a number of articles that go over REST basics:

I’ve been working with the REST module for a little while now, and I’d like to share some insights and snippets that I’ve gathered throughout the process of integrating an Angular web app with feeds powered by the REST module.

1 - Identify/enforce patterns in your frontend and build API accordingly

This part is much more challenging than any of the technical issues I encountered. In short, if it is at all possible, wait until you have a good chunk of your frontend UX pattern space defined before trying to build your REST API. Conversely, try to structure your frontend in a component-driven way so that your API will need to be responsible for fewer “one-offs.”

Once you identify the set of UX components that need data from the API, try to identify common patterns across different components. For example, one pattern might be “information about a single node” or maybe “a list of node titles related to a single node by an entity reference.” Distinctions like these will serve well as concepts for REST resource parent classes.

Then, once you’ve got a good idea of the UX patterns that you need to support, you can choose to use default REST module endpoints, REST Views, or custom resource class trees to get the job done.

2 - Enable the CORS module

Unless you’re connecting to the Drupal REST API from pages that share its domain, you’re going to need the CORS module. Fortunately, it works, at least for relatively simple directives. For example, I added: *|http://localhost:5000 to the configuration textarea on /admin/config/services/cors, and this was sufficient for it to authorize client-side requests originating from my local frontend dev environment.

3 - Add a results count to REST views

For a pager on a decoupled app to work, you need to know the size of the result space. Otherwise, your “next” button will not appear/disappear when you need it to, which can be annoying. Unfortunately, the REST module does not have an option to send a results count along with its response. Fortunately, it is pretty simple to make it work.

To start, make sure your view is using the Full Pager. Do not use Mini Pager - it has a post-execute hook that in many cases changes the pager count statistic to “PHP_INT_MAX / 2” which is a large and silly number. The intent here was to handle situations where the query returns too many results, but I don’t think it is detecting that properly. Anyway, just use Full Pager for now because you won’t see any of the markup.

Now you need to implement a views plugin. After a bit I decided to use the style plugin to inject our row count, extending the Serializer class provided by the REST module. The style plugin seems like an odd place to do this. However if you look at Drupal\rest\Plugin\views\style\Serializer, you’ll notice that it is doing more than just applying a processor to the data. It is actually aggregating the data as it loops over the views results and builds the $rows variable, so there isn’t actually a better point upstream where we can affect what gets serialized. It’s not a huge deal, but it means we have to duplicate some code. Here is what your new style plugin should look like:

<?php

/**
* @file
* Contains \Drupal\mymodule\Plugin\views\style\SerializerCount.
*/

namespace Drupal\mymodule\Plugin\views\style;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\style\StylePluginBase;
use Drupal\rest\Plugin\views\style\Serializer;

/**
* The style plugin for serialized output formats.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
*   id = "serializer_count",
*   title = @Translation("Serializer with count"),
*   help = @Translation("Serializes views row data using the Serializer component and adds a count."),
*   display_types = {"data"}
* )
*/
class SerializerCount extends Serializer {

 /**
  * {@inheritdoc}
  */
 public function render() {
   $rows = array();
   $count = $this->view->pager->getTotalItems();
   // If the Data Entity row plugin is used, this will be an array of entities
   // which will pass through Serializer to one of the registered Normalizers,
   // which will transform it to arrays/scalars. If the Data field row plugin
   // is used, $rows will not contain objects and will pass directly to the
   // Encoder.
   foreach ($this->view->result as $row_index => $row) {
     $this->view->row_index = $row_index;
     $rows[] = $this->view->rowPlugin->render($row);
   }
   unset($this->view->row_index);

   // Get the content type configured in the display or fallback to the
   // default.
   if ((empty($this->view->live_preview))) {
     $content_type = $this->displayHandler->getContentType();
   }
   else {
     $content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
   }
   return $this->serializer->serialize(['results' => $rows, 'count' => $count], $content_type);
 }
}

Most of that render method is copied from its parent. You’ll notice though that it injects a row count, and organizes the data into “results” and “count.” Put this class into [path-to-mymodule]/src/Plugin/views/style/SerializerCount.php and you should be good to go.

4 - Know the $_GET parameters for Views paging

Odds are good that you’ll need to be able to control pagination parameters for REST Views via the URL. The following are $_GET parameters that affect how Views paging works:

  • page - set the page that you want to display. If you put in a page greater than the last page with results, you will get no results.

  • offset - determine where you want your result set to start. You have to select “Allow user to specify number of items skipped from beginning of this view” in the settings for the pager for this to work.

  • items_per_page - essentially this is the “limit” field that you’re used to in mysql. You need to check “Allow user to control the number of items displayed in this view.”

For any of these to work, you need to give your View the Full pager.

5 - Use machine names for content that needs to be addressed specifically

A general rule of thumb for me is that client apps should never have hardcoded node ids. Instead, add a machine name field to content types that might be requested specifically by the frontend. Basic pages are a good example of this. Then, you just need to write a resource that gets content by machine name.

In Drupal 7, I mainly used the Safeword module to manage machine names. That module has not been ported to Drupal 8 though, so in the meantime you can just use a text field with some validation.

6 - Use the right namespace annotation for POST resources

This sounds like it should be super easy, and for the most part it is. But there’s a secret to it though that made me have to do a lot of digging.

I had found an example of a REST resource that implemented post() in \Drupal\rest\Plugin\rest\resource\EntityResource and was using that as a template. I made the rookie mistake of thinking I could eyeball it instead of copying the whole file and taking out what I didn’t need. Anyway, it just would not work and I had to go down the debugging rabbit hole. Half a day later, I found it - a line in the annotation that I had dismissed as being just metadata was to blame.

For a POST resource to work, you absolutely must have:

"https://www.drupal.org/link-relations/create" = "[some path]"

.. within the uri_paths setting. [some path] is what will serve the POST. What’s happening here is it’s forcing you to define a path for the “create” link-relation namespace (as opposed to the canonical namespace which all of the other protocols require). Drupal is basically saying that since a POST may represent an insert, it needs to define a path to satisfy the requirement for that namespace. Here is an example of a POST resource that works:

<?php

/**
* @file
* Contains \Drupal\mymodule\Plugin\rest\resource\TestPostResource.
*/

namespace Drupal\mymodule\Plugin\rest\resource;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
* Represents entities as resources.
*
* @RestResource(
*   id = "test_post",
*   label = @Translation("Test Post"),
*   uri_paths = {
*     "canonical" = "/api/test",
*     "https://www.drupal.org/link-relations/create" = "/api/test"
*   }
* )
*
* @see \Drupal\rest\Plugin\Derivative\EntityDerivative
*/
class TestPostResource extends ResourceBase {

 /**
  * Responds to entity POST requests and saves the new entity.
  *
  * @param \Drupal\Core\Entity\EntityInterface $entity
  *   The entity.
  *
  * @return \Drupal\rest\ResourceResponse
  *   The HTTP response object.
  *
  * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  */
 public function post() {
   $response = new ResourceResponse(array('test' => 'ping'));
   return $response;
 }
}

Put this class in [path-to-mymodule]/src/Plugin/rest/TestPostResource.php

7 - Make your custom resource invalidate its cache intelligently

In my opinion one of the most stupendous innovations in Drupal 8 is the Cacheability Metadata API. It is a very complex subject, but all you need to know when working with REST module resources is that you can tell the resource cacheability to depend on objects (like nodes) that have cache contexts and/or tags. As an example, the following snippet shows how to tell your resource to invalidate its cache when something about a node within its scope changes:

<?php

/**
* @file
* Contains \Drupal\dp_rest\Plugin\rest\resource/ContainerRestResource.
*/

namespace Drupal\dp_rest\Plugin\rest\resource;

use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\Core\Session\AccountProxyInterface;
use \Drupal\node\Entity\Node;
use Drupal\File\Entity\File;

/**
* Class ContainerRestResource
*
* @RestResource(
*   id = "content_container",
*   label = @Translation("Content container"),
*   uri_paths = {
*     "canonical" = "/api/page/{nid}"
*   }
* )
*/
class ContainerRestResource extends ResourceBase {

 /**
  * {@inheritdoc}
  */
 public function get($nid) {
   $page = node_load($nid);
   $response = new ResourceResponse(['nid' => $nid]);

   // Make the response use the node's cacheability metadata.
   $response->addCacheableDependency($page);

   return $response;
 }
}

The fact that this works is pretty cool.

8 - Know your options if REST module isn’t working

In my experience, the REST API works well but it does have some quirks, and at times, I have gotten stuck on weird issues (take the super-secret POST annotation requirement for example). If you are in a tight spot and REST API is giving you a lot of problems, you can take some comfort in the fact that Drupal 8’s (or really that is to say Symfony 2’s) routing API is quite good and provides nice stuff like protocol mapping and controller class delegation. The REST module provides a nice layer of abstraction onto this so your code will be a lot neater, but at the end of the day you can build REST APIs with the regular routing system too.

Amusingly, at the time when I was writing this, the top search result for “custom rest resource drupal 8” was this article which does not use the REST API.

For smaller one-off implementations where REST module abstractions might have you stuck in the mud, feel free to consider this approach. However, if you’re building a larger API, I’d recommend digging into REST module and learning it, since without it you’ll be writing a good deal of code to solve problems and organize things that the REST team has already tackled.

Conclusion

One complaint I’ve heard a lot about the D8 REST module is that the endpoints it provides out-of-the box are less than ideal. I agree with this for the most part: especially, the entity GET resources are needlessly bloated when it comes to the fields they return. But that is okay, since the REST module gives you a great set of tools to build your own solution. I hope some of the snippets and strategies I have described here can help with some of the stickier parts!

Additional Resouces
Supplying Thumbnails to Your Headless Drupal Frontend Blog
How to Add Content in an Update Hook | Blog

Redirects
/blog/eight-insights-and-useful-snippets-d8-rest-module