"REST in Core" text with computer mouse hand icon

D8FTW: REST in Core

Drupal 8 core ships with a trio of modules that enable push-button support for offering up content as a web service. Larry Garfield explains in our third installment of our Web Services in Drupal 8 blog series with Acquia. 

Drupal 8 core offers a routing and request handling pipeline that gives developers more control over how to handle an incoming request than ever before. Developers can route an incoming request based on any HTTP property, or even derived information. Controllers can return page bodies, full responses, domain objects that can be turned into full responses, or anything else PHP supports.

That's great, but doesn't that mean we have to then, um, do all of that ourselves? We can, and many times we should, but in many cases we don't have to!

Core ships with a trio of modules that enable push-button support for offering up content as a Web service, and (you guessed it) can be enhanced via contrib.

Serialization

The first is the Serialization.module, which in turn is built on the Symfony Serializer component. The Serialization module offers a standard way to convert a classed object to a serialized string and back again. That process consists of two phases: A Normalizer, which converts between an object and a known nested-array structure (and back again), and an Encoder, which converts between that array structure and some string format (and back again). A serializer object contains a set of normalizers and encoders, and can figure out on the fly which to use.

The Serializer includes normalizers for content entities as well as JSON and XML encoders. That means core provides clean round-trip support between any content Entity and any defined output format. That is, once you pass the serializer service as a dependency to your code you can simply do this:

<?php
$json
= $this->serializer->serialize($entity, 'json');
$xml = $this->serializer->serialize($entity, 'xml');
?>

Poof, we now have a JSON-ified version of an entity and an XML-ified version of the entity. And we can go the other way, too:

<?php
$entity
= $this->serializer->deserialize($json, Node::class, 'json');
?>

The net result is that we now have a standard universal serialized format for all entities! Or at least for their internal structure, which is appropriate in some cases but not all.

It's also straightforward to write new Normalizers and Encoders, as they're simply tagged services with a defined interface. Another core module, HAL.module, provides serializers and encoders using the Hypertext Application Language format, a special flavor of JSON (or XML) that includes hypermedia links as well.

REST Resources

The third core module is REST.module. REST module uses the core plugin system to define "rest resource" plugins. Each resource can live at a defined path pattern, which implies one or more routes at that path, and has separate methods for handling each HTTP method it supports. Resources do not need to correspond to any other Drupal object; they can, but there's nothing inherent in them that requires them to do so. That's good, because REST resources need not, and often should not, correspond to underlying objects in the system.

A method on a REST plugin acts as a controller, and while it can return any value it generally will return a special subclass of Response called ResourceResponse that will handle serializing a data object provided on it as well as setting appropriate cache tags. Core provides two resource plugins, one for content entities, the most likely to be used, and one for database logs, mostly just to prove that it can be done. In fact, the database log resource is quite simple:

<?php
/**
* Provides a resource for database watchdog log entries.
*
* @RestResource(
*   id = "dblog",
*   label = @Translation("Watchdog database log"),
*   uri_paths = {
*     "canonical" = "/dblog/{id}"
*   }
* )
*/
class DBLogResource extends ResourceBase {

 
/**
  * Responds to GET requests.
  *
  * Returns a watchdog log entry for the specified ID.
  *
  * @param int $id
  *   The ID of the watchdog log entry.
  *
  * @return \Drupal\rest\ResourceResponse
  *   The response containing the log entry.
  *
  * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  */
public function get($id = NULL) {
  if (
$id) {
   
$record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
      ->
fetchAssoc();
    if (!empty(
$record)) {
      return new
ResourceResponse($record);
    }

    throw new
NotFoundHttpException(t('Log entry with ID @id was not found', array('@id' => $id)));
  }

    throw new
HttpException(t('No log entry ID was provided'));
  }
}
?>

In this case, all that's provided is GET support. POST or PUT requests will automatically be rejected with an HTTP 405 (Method Not Allowed) error. The resource is exposed at the URI /dblog/{id}. And all it does is read back a single record out of the watchdog log as an array, which will get normalized to JSON or XML or whatever was requested by the serializer. (You likely shouldn't enable log Web service resources in production, but it's fun to play with on your own server to get a feel for how the system works.)

The plugin for content entities leverages the entity's already-defined path, so that the serialized version of an entity, such as a node or taxonomy term, lives at the same path as the HTML version of it. They are the same underlying object so should be exposed as just different representations of the same resource.

REST UI

For various reasons mainly related to available development time, the UI for REST module lives in contrib. The REST UI module provides a basic UI for configuring which REST resources should be enabled, which methods should be allowed, which formats are allowed, and which authentication mechanisms are allowed. (Core offers cookie-based auth and HTTP Basic Auth, the latter of which is only ever safe over SSL. Contrib also offers an OAuth module.) See the screenshots below.

Screenshot of REST resources

Screenshot of Settings for resource Content

So for example, we can enable GET JSON requests for Taxonomy terms, GET and PUT HAL requests for Nodes, and not expose anything else as an API; and just by checking a few boxes. (Isn't that the definition of success in Drupal, just checking a few boxes?)

Caveats

There are two limitations of the core REST support that are important to mention. A moment ago we said that we use the same path for both the HTML and JSON/XML/Whatever version of an entity. That's true, but we don't, technically, use the same URI.

HTTP, by design, allows a request to specify what formats they're willing to accept for a resource, using the Accept header. The server will then compare that list against what it knows it can offer and send back the best option or an error that it cannot find a workable format. However, sending back different formats from the same URI creates a caching problem, as any proxy servers or browsers that try to cache the response can cache whichever format is requested first, then send that format (wrongly) in the future on other requests. Again, the spec has a simple solution here: The Vary header on the Response, which can be used to tell clients and proxies to use both the URI and the Accept header to determine if a request matches a cached response. Problem solved, right?

Well, it would be if clients followed the spec. Unfortunately there's a number of issues in practice:

  • Most browsers send a huge mish mosh of possible formats in their Accept header, which always ends in */*, meaning "or send whatever". And every browser sends a different mishmosh, and even different versions of the same browser may do so. That means just blindly caching on the Accept header would result in different cache entries for, potentially, every browser and version that visits the site. Not good.
  • Most reverse proxies and CDNs don't obey the Vary header by default. Most can be configured manually to use additional headers for caching, and some can be set to obey Vary, but not always.
  • Some versions of some browsers, most notably Safari, disable their own cache when the Vary header has a non-default value rather than making their cache smarter.

The result is that we're in one of those cases where "no one follows the spec, which means no one can follow the spec." Accept-header based negotiation is basically broken on the web unless you control both the client and the proxy server. Sad panda.

Instead, core recognizes an extra query parameter in requests to indicate the format requested. A query parameter of _format indicates what registered format the request wants, with HTML being the default. That means the first request below will return a HAL JSON version of a specific node, the second an XML version, and the third will return the normal HTML page. (It's rare that you'll ever specify HTML explicitly as it's the default.)

http://example.com/node/5?&_format=hal_json
http://example.com/node/5?&_format=xml
http://example.com/node/5?&_format=html (default)

It is still possible with only a little work to do proper Accept-header based negotiation in cases where you know it will work, but speaking to web browsers is not one of them.

The other caveat is Hypermedia Links. Recall we said in part 1 of our series that hypermedia links between resources is a key part of a fully RESTful API. At this time, core only defines a few links for entities, and those are not exposed in the HAL representation. Again, that was mainly due to time. A plan to implement more robust link handling, including automatically extracting link relationships from Entity Reference fields, has been defined but was deferred until after 8.0.0. For now, all that's missing is someone spending the time to implement it. Volunteers welcome. (Yes, that means you!)

Of course, if REST module's design doesn't suit your use case that's fine. You can still build directly off of the routing system to construct whatever API you want using the tools Drupal offers. Or, other contributed modules can offer an alternate experience for building out an API in a more clicky way. More on that in part 4…

A note about this series: Web Services in today's applications and websites have become critical to interacting with third parties, and a lot of Drupal developers have the need to expose content and features on their site via an API. Luckily for us, Drupal 8 now has this capability built right into Core. Some contrib modules are attempting to make such capabilities even better, too.

To shed some light onto these new features, we've worked with Acquia to develop a webinar and subsequent series of blog posts to help get you up to speed with these exciting, new features. The first of these blog posts, Web Services 101, has been published on the Acquia Developer Center previously, written by our very own Senior Architect and Community Lead Larry "Crell" Garfield, with his follow-up on our blog here.

Larry follows up with this post in the series describing in detail REST in Drupal 8 Core. Look for subsequent posts on Acquia's Developer's blog soon.

Written by Larry and Kyle Browning of Acquia, all parts of this series are based on a webinar they presented previously: Drupal 8 Deep Dive: What It Means for Developers Now that REST Is in Core