Playing with Web Services API in Drupal 8. Theory and practice.

One problem I often see - all those new fancy tutorials and how-tos on RESTful Web Services API in Drupal 8 end up with building a simple React/Ember/Angular/YouNameIt application, this way showing all the power and mightiness of our favourite platform.

No surprise here, cause this is exactly why we need services in the first place.

However, as useful as those writings are (and I'm a big fan of them), most have one thing in common - too little attention to Drupal, and too much attention to explaining the external application. As a result, we're scratching the surface of both worlds (Drupal and JavaScript most of the time), but leaving readers with lots of questions on how actually Services in Drupal 8 work and how they can be configured to feed/consume the data to/from ANY external application imaginable.

Because in the end, we, as content producers, don't have a clue about the reader's setup - could be he/she uses React or Vue.js, fully decouples a system or uses progressive decoupling, or maybe it's just two Drupal sites talking to each other. We don't know.

So, in my opinion, we should focus more on empowering readers with the knowledge they need to create a reliable web service for their website, and if needed, explain all the intricacies of an external (non-Drupal) app configuration separately.

Following this, in the small set of tutorials, I will try to focus on what Drupal's new RESTful Web Services API has to offer out of the box, with minimum dependencies and in a very abstract way, so that you later decide what to do with the knowledge.

Instead of talking too much we'll be playing all the time - building concrete examples, sending requests and doing other fancy things. In the end, you should have a good understanding on how to configure your Drupal installation to work with ANY external application via RESTful API.

RESTful what?

To understand better what we are talking about let's split the whole thing into two parts:
1. Web Services allow computers to communicate over the network, most of the time via HTTP;
2. REST (a.k.a. REpresentational State Transfer) is just one of many methods of implementing web services.

A web service is RESTful if it follows five base principles (last one is optional):
1. Client-server relationships are established via usage of Resources. In a typical scenario, a client connects to a resource URI requesting some data, which is returned from the underlying database;
2. Statelessness. In our scenario, once the data is returned to a client, a server immediately "forgets" everything about the previous request and the query to the resource should be formed as the previous one never existed. There is no session state to keep on the server, and all the data needed to return proper data should be sent together with each request;
3. All parties involved in communication process should be aware of cacheability. Things that can be cached should be cached, and vice versa;
4. A uniform interface exists to ensure all components in the system are compatible between each other and know how to communicate;
5. Layered nature. Like mentioned above, a client connecting to an intermediate layer, i.e. a proxy, should not know about the underlying resources, databases and whatever magic powering your system;
6. Code on demand. An optional component, which is best explained by a server sending a piece of JavaScript code to a client, this way temporarily modifying client's behaviour. Almost never used in Drupal world.

Alright, no more talking, let's see how those principles above apply to a real world scenario. For the sake of this tutorial let's assume we're starting from scratch.

Download Drupal

Make sure you have Composer available in your system. Then run:

composer create-project drupal-composer/drupal-project:8.x-dev DIR --stability dev --no-interaction

Replace DIR with the name of the folder where you wish to install Drupal.

Install Drupal and the required modules

To install Drupal you should have a way of running LAMP/LEMP apps on your machine (Docker, MAMP, Acquia Dev Desktop, native installation etc.). And of course, you need drush.

From the Drupal folder:

drush si standard --db-url=mysql://root:root@localhost/drupal8 --site-name="Drupal 8 API Test" --account-pass=admin

We use Standard profile here just to have all necessary admin menus ready for tinkering with the system right after the install.

From the root folder:

composer require drupal/devel:^1.0 drupal/restui

From the Drupal folder again:

drush en restui devel_generate -y

It will enable all the required modules.

Enable and configure the "Content" resource

Now, if you installed Drupal with my command, login with admin:admin username and password and visit the admin page:

/admin/config/services/rest

Enable "Content" resource. Enable all methods, select "json" as the only acceptable request format and enable "cookie" authentication.

You will end up with something like this on the screenshot.

Time for a coffee break. A note on authentication methods

You may wonder why we enabled "Cookie" authentication instead of
"HTTP Basic Authentication" provided by the corresponding module included in the core.

In general, both authentication methods follow REST principles, as our web service remains stateless all the time.

With "cookie" authentication method we have to generate and store the session cookie on a client (such as a local browser) to send it to a server with EVERY request we make.

With "basic auth" method we will be sending username and password in a weakly encrypted format in Authorization header with EVERY request we make.

I have chosen "cookie" just because it's easier to get and work with local development tools.

For production websites you may want to use OAuth or other methods where we don't have to expose our username and password at all.

Generate sample content

Back to business. Time to generate some sample content to play with. From the Drupal folder:

drush genc 10

Choose your client

We know already that our Drupal site is essentially a "server" providing "resources" for a client to obtain the data from an underlying database. Now, what is a client?

Think of a client as any possible consumer of the server data. It can be a JavaScript or PHP application, cURL, a browser or a dedicated desktop app.

For testing with a browser I highly recommend Postman (Chrome) or Poster (Firefox).

I will be using my favourite desktop app for Macs called Paw.

GET your first node

So, to receive our first node in JSON format, we need to craft a GET request to this node:

GET /node/1?_format=json HTTP/1.1  
Content-Type: application/json  
Host: drupal8.localhost:8888  

Note the Content-Type header and the _format=json Url query you have to supply along with your request.

So far so good. We've got our node as expected.

Authenticate a user

Now that we want not only to be able to receive data from the database, but also publish something, things get more interesting, as by default you can't create nodes as an anonymous user.

We will use cookie authentication here as you only need to send username and password once.

Both basic and cookie authentication methods should not be used on production websites as described in this tutorial. You should rather be using OAuth via the Simple OAuth module or another authentication provider to avoid exposing your credentials (even via HTTPS and even when poorly encrypted).

Craft the new POST request with the same headers and URL query params, but supplying our username and password in the body of the request, like so:

POST /user/login?_format=json HTTP/1.1  
Content-Type: application/json  
Host: drupal8.localhost:8888  
{"name":"admin","pass":"admin"}

In response we receive the following data, which contains everything we need to continue working with the website via an API as an authenticated user.

Create a node

Our endpoint for this /entity/node.

Note that we're sending the session cookie obtained from the previous login request together with X-CSRF-Token header. This header value can be seen under csrf_token key in the response above.

POST /entity/node?_format=json HTTP/1.1  
Content-Type: application/json  
X-CSRF-Token: --R5bH74hYI1uamdFZmHQX9v9-MVuYN1CPsj-7k19GA  
Cookie: SESS47b9f626f4144d455bf42a7fcbe55ada=akh1IyzbqaBN-2zVMMjj9ezYoUbWzluHexk6xnmRVGg  
Host: drupal8.localhost:8888

{
  "langcode": [
    {
      "value": "en"
    }
  ],
  "type": [
    {
      "target_id": "article",
      "target_type": "node_type"
    }
  ],
  "status": [
    {
      "value": true
    }
  ],
  "title": [
    {
      "value": "Title of our new node"
    }
  ],
  "promote": [
    {
      "value": true
    }
  ],
  "sticky": [
    {
      "value": false
    }
  ],
  "body": [
    {
      "value": "Here goes the content of our new node!",
      "format": "plain_text"
    }
  ]
}

And here is our node!

Phew, what a journey!

Next time we'll try to play with Views a bit and touch some more advanced concepts.

Have any questions or something is unclear or is not working? Shout in the comments!