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!
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.
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" } ] }
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
).
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:
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.
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.
Then, we attach our changes in the request body.
Here is what we receive in return, a 200 OK
response with the body containing the serialized entity, indicating a successful PATCH
:
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!
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:
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:
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!
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
- "Getting started: REST configuration & REST request fundamentals." Drupal.org. 17 May 2017. Accessed 2 April 2018.
- Garfield, Larry. "Putting off PUT." Drupal.org. 26 February 2013. Accessed 2 April 2018.
- Wehner, Daniel. "Accept header based routing got replaced by a query parameter." Drupal.org. 6 July 2015. Accessed 2 April 2018.
- "POST for creating content entities." Drupal.org. 14 March 2018. Accessed 24 April 2018.
- "PATCH for updating content entities." Drupal.org. 9 November 2016. Accessed 24 April 2018.
- "DELETE for deleting content entities." Drupal.org. 11 July 2017. Accessed 24 April 2018.