One of the nicest new features of Drupal 8 is its support of REST out of the box. With more and more websites moving to a decoupled approach, whether fully or partially, it has become essential for developers to understand how to build REST endpoints.

Luckily, Drupal makes this relatively easy to do with its support of REST baked into core. A lot of Drupal’s REST support relies on what is provided by the Symfony library. Which is quite a bit. But using the Drupal wrappers on Symfony’s Response classes adds support for Drupal’s extended caching features such as cache tags and cache contexts which Symfony knows nothing about. This is particularly useful in environments that use caching mechanisms such as varnish.

For more details on Drupal’s cache API, see https://www.drupal.org/docs/8/api/cache-api/cache-api.

At DrupalCon Baltimore, an excellent presentation was given on building endpoints. The presenters, Erin Marchak and Justin Longbottom, live-coded a decoupled React application using Drupal as a backend. It was informative and helpful for anyone looking to work with Drupal at any level of decoupling. Check out their presentation at the DrupalCon Baltimore site.

Having created REST resources in Drupal 8 before, I confess that I hadn’t worked with it as they presented it. Many of my projects at Mediacurrent had specific requirements that Drupal’s default REST framework wouldn’t have satisfied. Even so, there could be reasons to use Drupal’s REST framework over a more custom solution. And that will vary from project to project based on need.
 

Drupal’s REST vs. Custom REST

Drupal gives you a lot out of the box. Creating a resource is as simple as using Drupal console (drupal generate:plugin:rest:resource) or creating a new plugin file and declaring your endpoint URL in the docblock:

/**
* @RestResource(
*   id = "test_rest_api_drupal_resource",
*   label = @Translation("Test rest api drupal resource"),
*   uri_paths = {
*     "canonical" = "/api/test-rest"
*   }
* )
*/

This will create a route to your endpoint at /api/test-rest. This new REST resource plugin will also, by default, have the ability to support each type of request that can be made: GET, POST, PUT, etc. But there are a couple of things you have to do first.

  1. Enable your endpoint. This can be done via Drupal Console (drupal rest:enable) or via the contributed module REST UI (https://www.drupal.org/project/restui). Unless you enable your endpoint, it will not be available to serve data.
  2. Set permissions to allow access. By default, once the new REST endpoint is enabled, only the Administrator role has access to view your new endpoint. You’ll need to set the permissions for the endpoint to allow access to whichever users should be able to view its results.

Once complete, your endpoint will be accessible. One important thing to note is that simply accessing /api/test-rest will not work. REST resources created with Drupal’s core REST framework require an additional parameter at the end. In my case, I enabled the output to be formatted in JSON. So my endpoint should be /api/test-rest?_format=json.

And that’s it. Pretty cool and relatively easy to do.

But there are cases when this would not be a workable solution.

  1. The client has predefined endpoints that must be matched. This is common on new builds when clients have previously built applications and processes that rely on the endpoints provided by Drupal to match whatever is already in-place. In those instances, adding the _format=json parameter isn’t an option.
  2. The client relies on more complex permissions. In some cases, there may be situations where the single permission provided by Drupal isn’t enough to cover when a user should have access to a custom endpoint. Custom permissions are simple to define in Drupal 8, but applying them to a REST endpoint is not as straightforward.

This is when a custom REST implementation might make more sense.
 

Creating a Custom REST Endpoint

Creating a custom endpoint requires the following:

  1. A route
  2. Any permissions needed
  3. A controller
     

The Route

In a custom module, define a route for your custom endpoint.

custom_rest_module.get_latest_nodes:
 path: 'api/rest-endpoint/latest'
 defaults:
   _controller: '\Drupal\custom_rest_module\Controller\CustomRestController::getLatestNodes'
 methods: [GET]
 requirements:
   _access: 'TRUE'

This endpoint will allow anyone to access it. To add permissions, simply swap out

   _access: 'TRUE'

With

   _permission: 'access custom rest endpoint'

Or

   _permission: 'access custom rest endpoint,view own article content'

and so on. For more detail on configuring your route, see the documentation on Drupal.org.
 

The Permission

Creating a custom permission is straightforward in Drupal 8. Simply create a YAML file at the base of your module with the name <your_module>.permissions.yml. In our case, since my demo module is called custom_rest_module, the file would be named custom_rest_module.permissions.yml. Inside, the contents would look something like:

access custom rest endpoint:
 title: 'Allow access to the custom REST endpoint'

Adding a custom permission is not required to make this endpoint serve data. However, it is always a good idea to understand who will be accessing the endpoint and more importantly, if they should be allowed access.
 

The Controller

Once that setup is complete, you can either use Drupal Console to generate your controller (drupal create:controller) or write one by hand. I generally have a base of controllers that I reuse and find very helpful.

Here is a sample controller for this example:

<?php

namespace Drupal\test_rest_api\Controller;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\Query\QueryFactory;

/**
* Class CustomRestController.
*/
class CustomRestController extends ControllerBase {

 /**
 * Entity query factory.
 *
 * @var \Drupal\Core\Entity\Query\QueryFactory
 */
 protected $entityQuery;

 /**
 * Constructs a new CustomRestController object.

 * @param \Drupal\Core\Entity\Query\QueryFactory $entityQuery
 * The entity query factory.
 */
 public function __construct(QueryFactory $entity_query) {
   $this->entityQuery = $entity_query;
 }

 /**
 * {@inheritdoc}
 */
 public static function create(ContainerInterface $container) {
   return new static(
     $container->get('entity.query')
   );
 }

 /**
 * Return the 10 most recently updated nodes in a formatted JSON response.
 *
 * @return \Symfony\Component\HttpFoundation\JsonResponse
 * The formatted JSON response.
 */
 public function getLatestNodes() {
   // Initialize the response array.
   $response_array = [];
   
   // Load the 10 most recently updated nodes and build an array of titles to be
   // returned in the JSON response.
   // NOTE: Entity Queries will automatically honor site content permissions when
   // determining whether or not to return nodes. If this is not desired, adding
   // accessCheck(FALSE) to the query will bypass these permissions checks.
   // USE WITH CAUTION.
   $node_query = $this->entityQuery->get('node')
     ->condition('status', 1)
     ->sort('changed', 'DESC')
     ->range(0, 10)
     ->execute();
   if ($node_query) {
     $nodes = $this->entityTypeManager()->getStorage('node')->loadMultiple($node_query);
     foreach ($nodes as $node) {
       $response_array[] = [
         'title' => $node->title->value,
       ];
     }
   }
   else {
     // Set the default response to be returned if no results can be found.
     $response_array = ['message' => 'No new nodes.'];
   }

   // Add the node_list cache tag so the endpoint results will update when nodes are
   // updated.
   $cache_metadata = new CacheableMetadata();
   $cache_metadata->setCacheTags(['node_list']);

   // Create the JSON response object and add the cache metadata.
   $response = new CacheableJsonResponse($response_array);
   $response->addCacheableDependency($cache_metadata);

   return $response;
 }

}

Accessing this class will generate a JSON object with the titles of the 10 most recently updated nodes. The details available in this response are minimal, but can be expanded to include more details from the node itself. Additionally, the results could be duplicated using a direct database query. For an example of that method, see the presentation from DrupalCon Baltimore that was referenced earlier in this post.
 

Moving Forward

There is currently an open issue on Drupal.org to allow setting a default response format on core’s REST responses. This would eliminate the necessity of setting the _format=json parameter in requests. Should this make it into core, it is possible that this use case could be covered by Drupal’s REST framework without having to make a custom controller.

However, some developers might not like having to create a new class for each REST resource they need. With a custom controller and routes, the same controller could be used multiple times when defining endpoints. All administration of permissions and routes would be contained in a single routing.yml file. And access to routes could easily be tied into existing permissions without the need for any further customization.

Not every project needs this level of customization. Some projects could be creating new endpoints altogether. And perhaps JSON API or GraphQL would be a better fit. A lot can be accomplished with Drupal’s core REST framework. But if you want fine-grain control over every portion of your endpoint, a custom REST endpoint could be the best solution.

Helpful Links:

Additional Resources:
8 Insights and Useful Snippets for the D8 REST Module | Blog
Supplying Thumbnails to your Headless Drupal Front End | Blog
Drupal 8: Restful Services | Presentation