Decoupling Drupal 8 Core: Retrieving and Manipulating Content with Core REST

As we saw in a previous installment of Experience Express, because Drupal has a HAL-compliant REST API available out of the box with minimal configuration, you can easily provision an API that can immediately be employed to consume content entities and manipulate them from other applications. Now that we have successfully exposed content entities as REST resources, used Entity Access to govern permissions, and customized the formats and authentication mechanisms in use by the core REST API, it is now time to move into actually retrieving and manipulating that data.

Luckily, if you are familiar with other REST APIs, issuing HTTP requests against Drupal core to obtain the data you require in your application is simple. In this column, we will inspect the key components of most requests to the core REST API, how to retrieve and update content entities via core REST, and how to create and delete them.

Issuing REST requests against Drupal core

Because REST is an architectural pattern that operates on HTTP, it makes use of HTTP verbs, which can be categorized into safe and unsafe methods. Often, however, this does not provide enough protection, so Drupal has provided an additional mechanism — the X-CSRF-Token request header — to ensure that unsafe methods cannot be used nefariously. Finally, because Drupal flexibly serves responses across a variety of serialization formats, a query argument must be provided that contains the appropriate serialization format.

Safe and unsafe methods

In HTTP, verbs — also known as request methods — include GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, and PATCH. Some of these request methods are safe in that they are read-only and cannot modify the data that they are retrieving. From the list above, safe methods include HEAD, GET, OPTIONS, and TRACE. Because all of the other methods are capable of performing write operations against the data exposed by the API, they are unsafe and can affect data stored in Drupal.

Among the methods listed above, we will only be considering GET, POST, DELETE, and PATCH in this blog post, because they correspond to the key CRUD (create, read, update, delete) operations that allow us to retrieve and manipulate content. In Drupal's case, GET means read, POST means create, DELETE means delete, and PATCH means update.1

While PUT in REST parlance also translates to updates of data, it is problematic because PUT requests typically include the entire data structure that will replace the existing data in the request body. As a result, requests that include not only a single content entity but also relationships to other entities introduce significant complexity to the API. Moreover, the HAL normalization in core includes link relations which must mirror the precise data returned when performing a GET request.

There are other Drupal-specific motivations for the lack of PUT support revolving around field-level permissioning on content entities. In order to perform a proper PUT request, the application performing the request would require write access on every field in a content entity rather than a select few. This means that permissions would potentially need to be expanded much more widely than expected. Fortunately, because PATCH supports partial write operations, updates to content entities can happen even when only certain fields are writeable by the consumer application in question.2

The X-CSRF-Token request header

Cross-site request forgery (CSRF) is a scenario in which a consumer application with permissions to manipulate data behind the API could issue malicious requests against the API, even without the consumer application's knowledge. This is because the consumer application may not necessarily have the protections required to filter out potentially harmful data contained therein, particularly if the request contains user-generated data.

In order to protect itself from CSRF attacks, Drupal 8 obligates all requests to define an X-CSRF-Token request header when issuing a request that is sent using an unsafe HTTP method such as POST, PATCH, or DELETE. The content of this request header should be filled in with the retrieved token from the path /session/token. In examples below, we will see how the use of unsafe methods differs from safe methods due to the presence of the X-CSRF-Token request header.

Specifying serialization formats

Because Drupal can flexibly handle multiple serialization formats, including HAL+JSON, JSON, and XML, all requests to the core REST API must include a query argument specifying the serialization format used in the request. This is the case even if only one serialization format is configured to be supported in the core REST API (such as JSON).

When issuing requests against Drupal's core REST API, you must append to each URI the query argument ?_format. For instance, on the test site that we installed previously, issuing a GET request to retrieve HAL-compliant JSON for a node with an ID of 1 would require us to use the URI core-rest.dd:8083/node/1?_format=hal_json.

When a request body contains data in a particular serialization format, which is the case of unsafe methods such as POST and PATCH, you also need to specify the Content-Type request header with the chosen serialization method. The examples below demonstrate how.

Note: While Accept header-based content negotiation previously existed in Drupal 8, it was removed from Drupal because of poor support on the part of browsers and proxies. Drupal 8 now requires the serialization format to be provided in query arguments rather than solely in the Accept request header.3

Retrieving content entities with core REST

If you do not currently have a test site like the one we set up previously, return to that previous installment of Experience Express to set up a site like core-rest.dd:8083. (All subsequent paths presented here will be domain-relative.) In addition, the examples below make use of the Postman HTTP client, which allows us to issue requests more quickly and with a friendlier interface than other tools like cURL. We already retrieved a content entity using GET previously, but it bears repeating to show the process once more. Issuing successful GET requests against the core REST API requires the following REST resource configuration:

granularity: resource
configuration:
  methods:
    - GET
  formats:
    - hal_json
  authentication:
    - basic_auth

In Postman, you can issue a GET request against /node/1 with the query parameter ?_format=hal_json — resulting in /node/1?_format=hal_json — without any headers and receive the following response with a 200 OK status code. Success!

image6.png

Creating content entities with core REST

Issuing successful POST requests against the core REST API requires the following REST resource configuration:

granularity: resource
configuration:
  methods:
    - POST
  formats:
    - hal_json
  authentication:
    - basic_auth

Before issuing our request, we must first craft our request body to include the data structure we want Drupal to use to create our new content entity. In order to do this with HAL+JSON, we will also need to include the correct _links key in the request payload. The easiest way to identify what value the _links entry needs to provide is issuing a GET request against the entity and studying the link relations in the response payload.

Importantly, the request payload should never include a UUID, as that is assigned by Drupal when the content entity is created.

POST requests in Drupal require two steps if you have not yet retrieved an X-CSRF-Token. First, using Postman, issue a GET request against /rest/session/token. In response, you will get a jumble of letters and numbers (e.g. Eh1INrGyEUNBog5ZL2o-dHFPnLoseIKCcL35aVSGg94); this is your X-CSRF-Token which should accompany every unsafe method you use.4 Copy it to your clipboard for future reference.

image5_0.png

To create a new article, for instance, we can craft a POST request that contains the data structure for a new article with the title My snazzy new article. First, we provide a request payload that contains a data structure that Drupal can interpret and use to create a new article:

{
  "_links": {
    "type": {
      "href": "http://core-rest.dd:8083/rest/type/node/article"
    }
  },
  "title": [
    {
      "value": "My snazzy new article"
    }
  ],
  "type": [
    {
      "target_id": "article"
    }
  ]
}
image1_6.png

At this point, because we are primarily focused on how to form a request, we will make our lives easier by not using any built-in authentication, which will be covered in a future installment. This is extremely dangerous and highly inadvisable live on production, but we can do this safely on a local production environment. While user-generated content is an exception, operations across the API by applications are typically riskier because it isn't possible for Drupal to control how or how much of that content is introduced.

For the purposes of this blog post, we'll enable an anonymous user to create content by navigating to People > Permissions in Drupal's administration toolbar. Grant the following permissions to the Anonymous user role on the appropriate content types (currently we are only working with Articles and Basic pages): Create new content (i.e. POST), Delete any content (i.e. DELETE), and Edit any content (i.e. PATCH).

Then, back in Postman, in the request headers, we will want to provide the X-CSRF-Token that we retrieved via GET earlier (X-CSRF-Token: Eh1INrGyEUNBog5ZL2o-dHFPnLoseIKCcL35aVSGg94) as well as the correct Content-Type header (Content-Type: application/hal+json).

image4_4.png

In Postman, you can issue a GET request against /node/1 with the query parameter ?_format=hal_json — resulting in /node/1?_format=hal_json — without any headers and receive the following response with a 200 OK status code. Success!

Now, in Postman, we can issue a POST request against /entity/node?_format=hal_json (recall that the query parameter is required for every request to Drupal). When we issue this request, we receive a 201 Created response whose payload contains the data structure of the entity we just created. Success again!

Here is what we're given by Postman:

image2_8.png

If we navigate to our home page, which currently displays content ordered by most to least recent, we can see our new article has been created by Anonymous. Because we did not provide anything in the body field, the article only has a title.

image7.png

Note: As of Drupal 8.3.0, it is possible to issue requests against the resource /node with the format query parameter included; for all intents and purposes that resource is identical to /entity/node in versions of Drupal 8.3.0 and later.4

Updating content entities with core REST

Now that we've created our snazzy new article, how do we update it in cases where our marketing team needs the title to change or our client would like slightly adjusted text? Issuing successful PATCH requests against the core REST API requires the following REST resource configuration:

granularity: resource
configuration:
  methods:
    - PATCH
  formats:
    - hal_json
  authentication:
    - basic_auth

As we saw with POST requests, before issuing our request, we need to ensure that our request body contains the _links key to adhere to the HAL specification. Because the entity has already been created, the request payload should not include a UUID, as that is an immutable value within Drupal.

PATCH differs from POST in that whereas creating content requires us to provide every field we wish to represent in the created node, PATCH only obligates us to provide the changes we want made to the entity. While this means that we have less data to transmit, and our requests can accordingly be smaller, it also presents several challenges due to the fact that a 403 Forbidden response code will never be issued when one field is editable based on permissions but another one defined in the same request is not editable. As a result, some requests could succeed partially and record their failures silently.

On the other side of the spectrum, there are certain components of a PATCH request which must be present, even if that information is not changing, because the server needs an understanding of which bundle to refer to when it deserializes that information. As such, certain information, for instance the content type (e.g. Article or Basic page), must be transmitted in the request.

Note: Since Drupal 8.1.0, all successful PATCH requests return a 200 OK response status code with the serialized entity in the response body. Before Drupal 8.1.0, you would receive a 204 No Content code with an empty body.5

To begin, we need to craft a PATCH request whose headers contain our X-CSRF-Token. In this example we've generated a new one (i7GUIxfEYRR3nzcNyremz9Q73sdyTpStSoCsU7J0NQw). Then, to update our existing article, we need to provide a request body that contains the fields we wish to change as well as the HAL-compliant _links key:

{
  "_links": {
    "type": {
      "href": "http://core-rest.dd:8083/rest/type/node/article"
    }
  },
  "title": [
    {
      "value": "My snazzy and snappy new article"
    }
  ],
  "type": [
    {
      "target_id": "article"
    }
  ]
}

In this case, because we are only changing the title field (our title is changing to "My snazzy and snappy new article"), we don't need to provide any other information about other fields. The target_id field provides the content type that Drupal will need to use to deserialize this data structure.

However, because we are not creating a new entity, the resource we point the request to changes to the resource that we created in the previous section. This means that we need to acquire the identifier of the node (the nid) and target our PATCH request against that resource URL. In our case, because we created twenty nodes previously, that path becomes /node/21?_format=hal_json, as our created article acquired an nid of 21.5

First, we provide the appropriate headers.

image4_3.png

Then, we attach our changes in the request body.

image8.png

Here is what we receive in return, a 200 OK response with the body containing the serialized entity, indicating a successful PATCH:

image12.png

If we refresh the Drupal home page, we see that the article we created before has now been updated with our new title: pretty snazzy and snappy!

image10.png

Deleting content entities with core REST

As it turns out, our customer is not a fan of the new article, despite its snazziness and snappiness. They've requested that we delete the article so that it does not appear in the consumer application. All content must go somewhere once it is no longer relevant or needed. For that, we need DELETE. Issuing successful DELETE requests against the core REST API requires the following REST resource configuration:

granularity: resource
configuration:
  methods:
    - DELETE
  formats:
    - hal_json
  authentication:
    - basic_auth

After retrieval of content with GET, DELETE requests may be the most simple to compose. This is because certain information is unnecessary; we do not need any deserialization on the Drupal back end, only an understanding of which entity we need to delete. In addition, because there is no request body, no data is ever transmitted, which means there is no need to have a MIME type encoded in a Content-Type request header.6

All we need to do is point our request at the resource in question with a suffixed query parameter — /node/21?_format=hal_json — and issue a DELETE request containing the X-CSRF-Token header. Here is what that request looks like; there is no screenshot of the request body, as that remains empty:

image11.png

Here is what we receive in return, a 204 No Content response status code with an empty body indicating that our entity is now deleted:

image9.png

Sure enough, when we navigate to our Drupal home page again and refresh the page, we can see that the entity has now vanished! Success!

image13.png

Conclusion

In this blog post, I introduced you to the key approaches in retrieving and manipulating content entities in Drupal through the core REST API employing simple use cases such as modifying a title or deleting an entity. As you can see, working with the core REST API is quite simple as long as you have configured permissions and REST resources correctly. While Drupal certainly is a unique case, and while these examples were trivial in order to demonstrate a consistent process to API testing that we will reuse later, the immediacy and directness of the core REST API are part of what make it so appealing.

Next time on the Experience Express, we're off to Philadelphia, where Drupaldelphia is part of the kickoff for Philly Tech Week. I'll be joining up with several others on a keynote panel about some of the exciting work happening around the future of Drupal 8 in response to the Driesnote at DrupalCon Nashville. You'll probably catch me having a roast pork sandwich (with sharp provolone and broccoli rabe, of course) at some point during the day!

Works cited

  1. "Getting started: REST configuration & REST request fundamentals." Drupal.org. 17 May 2017. Accessed 2 April 2018.
  2. Garfield, Larry. "Putting off PUT." Drupal.org. 26 February 2013. Accessed 2 April 2018.
  3. Wehner, Daniel. "Accept header based routing got replaced by a query parameter." Drupal.org. 6 July 2015. Accessed 2 April 2018.
  4. "POST for creating content entities." Drupal.org. 14 March 2018. Accessed 24 April 2018.
  5. "PATCH for updating content entities." Drupal.org. 9 November 2016. Accessed 24 April 2018.
  6. "DELETE for deleting content entities." Drupal.org. 11 July 2017. Accessed 24 April 2018.