How to Create a Drupal 8 Custom REST API
How to Create a Drupal 8 Custom REST API


I use Drupal 8 as headless backend for my web applications. Drupal waits for the frontend to ask it for information. The frontend shows the data and interacts with the user.

Drupal 8 allows me to create REST APIs writing no code. I just have to configure a view and the REST API comes free of charge.

But I may need a custom REST API sometimes. Let's see how to build one.

Defining the custom REST API

I first have to define a module so that Drupal knows about my REST API. Then I add the code that makes the API work, and finally I do testing.

Defining the module

I call the Drupal module rest_api_demo.

The module needs a folder where to live. I give it the same name as the module and put it under the folder {drupal root}/sites/all/modules/. In my case it's /srv/sites/backend.emanuelesantanche.com/sites/all/modules/rest_api_demo.

In this folder I create a file called rest_api_demo.info.yml.

It's the module's identity card.

name: Rest Api Demo Module
description: Demonstrating Drupal 8 custom REST APIs.
package: Custom
type: module
version: 1.0
core: 8.x

I specify the name and the description. I don't have to edit the entries version and core.

Adding the resource

I need to tell Drupal what the REST API does.

Under the module's folder, I create this subfolder: src/Plugin/rest/resource/.

On my server, I end up with this full path: /srv/sites/backend.emanuelesantanche.com/sites/all/modules/rest_api_demo/src/Plugin/rest/resource/.

I create there a php script called DemoResource.php.

<?php

namespace Drupal\rest_api_demo\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;

/**
 * Provides a Demo Resource
 *
 * @RestResource(
 *   id = "demo_resource",
 *   label = @Translation("Demo Resource"),
 *   uri_paths = {
 *     "canonical" = "/rest_api_demo/demo_resource/{type}"
 *   }
 * )
 */
class DemoResource extends ResourceBase {

  /**
   * Responds to entity GET requests.
   * @return \Drupal\rest\ResourceResponse
   */
  public function get($type = NULL) {
    
    if ($type) {
    
      // Getting node ids for node of type $type
      $nids = \Drupal::entityQuery('node')->condition('type', $type)->execute();
      
      if ($nids) {
          
        // Getting the actual nodes
        $nodes =  \Drupal\node\Entity\Node::loadMultiple($nids);
        
        foreach ($nodes as $key => $value) {
          // Getting the image field as object of class ImageItem
          $img_item = $value->get('field_image')[0];
      
          $url = "undefined";
          
          if ($img_item) {
            // Getting the FileItem object
            $img_file = $img_item->get('entity')->getTarget();
            
            // Getting the actual url where the image can be found
            if ($img_file) {
                $url = file_create_url($img_file->get('uri')->getString());
            }            
          }
          
          // Putting together the response for the current node
          $data[] = ['id' => $value->id(), 'title' => $value->getTitle(), 'image' => $url];
        }
      }
    }
    
    $response = new ResourceResponse($data);
    // In order to generate fresh result every time (without clearing 
    // the cache), you need to invalidate the cache.
    $response->addCacheableDependency($data);
    return $response;
  }
}

The name of the file is the same as the name of the REST resource, DemoResource.

I declare a namespace, Drupal\rest_api_demo\Plugin\rest\resource, so that I can give variables and functions the name I want without worrying that the same name may have been used in another module.

The namespace is pretty standard. The only part of it that changes is rest_api_demo, which is the name of the module.

The @RestResource declaration is not really a comment. Drupal needs it to know about the machine-friendly name of the resource, demo_resource.

In that declaration there is the path I have to use to query the REST API.

It's /rest_api_demo/demo_resource/{type} where {type} is the content type of the nodes Drupal has to fetch to populate the result.

The API queries any content type defined in the Drupal database. But I have only two content types that include a picture. Their machine names are ems_article and wrt_item.

A note about the function addCacheableDependency: it's to make it sure that I get the latest changes in the data without having to clear the cache.

Installing the module

Now I clear the cache and install the module.

The first task is simple, I just use drush.

root@FREEDOMANDCOURAGE:/srv/sites/backend.emanuelesantanche.com# drush cr

For the second task, I go to this page of the administrator's dashboard: https://backend.emanuelesantanche.com/admin/modules.

I search for rest_api_demo, select it and click on Install.

Drupal can now use the module.

Enabling the resource

I go to this page: https://backend.emanuelesantanche.com/admin/config/services/rest.

I see the resource Demo Resource listed.

I just have to enable it.

Giving permissions

But still non-admin users don't have permission to query the API.

I get here: https://backend.emanuelesantanche.com/admin/people/permissions.

I search for "Access GET on Demo Resource resource" and give permission to "Anonymous user" and "Authenticated user".

Testing time

I swing open a Firefox instance and visit https://backend.emanuelesantanche.com/rest_api_demo/demo_resource/ems_article?_format=json.

Firefox formats the json response for me, making it easy to read.

I do the same with https://backend.emanuelesantanche.com/rest_api_demo/demo_resource/wrt_item?_format=json.

Adding a front end

Let me add a front end. I'll use Svelte and Sapper.

Svelte is easy to use and fast. With the addition of Sapper, I can create applications very quickly and easily define routes. With some adjustments to the code, Svelte and Sapper make it easy to generate the application's html pages on the server and they are SEO friendly.

Using Docker

I use Docker for my development environments.

I don't have Node.js installed on my computer. Using Docker containers, applications can use the Node.js version they need and are not bound to a single one. I can have two applications: one using Node.js 12.0 and one using Node.js 10.0. The latter is not required to upgrade.

Docker containers isolate the application environment from the host machine. I don't have to fix the problems applications encounter when they have to fit an existing environment.

Making the docker image

When using Docker, I need to first create an image.

It's similar to what people do when they have to install the same software on hundreds of PCs.

They install a PC then they clone the hard disk bit-by-bit using specialized software.

The result is a DVD they copy to the hard disks of the other PCs.

Doing this there is no need to install each single PC. The copy runs automatically with little supervision required.

A Docker image is like the DVD and Docker containers are like the PCs.

After I create a Docker image, I can create as many containers as I want. They will be virtual machines running my applications.

I call the image nodejsgitbashnpm. The name reminds me that the image includes Node.js, git, bash and npm. With this image, I can create containers running applications that need Node.js.

The operating system this image is based on is Alpine Linux, a lightweight Linux that helps me produce small Docker images. The final image is 80 MB large.

To create this image I need a Dockerfile.

FROM alpine:3.11
COPY dockerfile.sh /
RUN /dockerfile.sh
CMD ["bash"]

When Docker creates an image, it starts a virtual machine, runs some commands in it installing some packages, shuts it down and freezes it. Later it's possible to use the image to create many containers that will be ready with the packages the image provides, but that can evolve independently from the image.

Some of them may run Svelte/Sapper, others React, etc.

The Dockerfile tells Docker how to build the image.

Alpine Linux is the foundation.

Docker copies the local script dockerfile.sh to the virtual machine's root path and executes it.

The image includes the disposition to run a bash instance.

When I create a container from this image, it knows that it has to run a bash shell.

Without this disposition, the container would start and stop immediately after.

This is the script dockerfile.sh.

#!/bin/sh

apk update

apk upgrade

apk add bash

# Making the version fixed tobe able to rebuild the image with no
# surprises
apk add nodejs=12.15.0-r1

# Includes npx
apk add nodejs-npm

apk add git nano

# Don't end this script with exit otherwise the container stops
cat << EOF > /root/.bashrc
#!/bin/bash
alias ll="ls -al"
export PS1="[\u@\h \W]\\$ "
EOF

It installs the needed packages and makes the shell more human-friendly adding to the script .bashrc the aliases and exports I couldn't live without.

With the following command, I create the image.

sudo docker build -t nodejsgitbashnpm .

I have to run it in the folder where there are the Dockerfile and the script dockerfile.sh.

Making the container

The command below creates a container using the image nodejsgitbashnpm.

sudo docker run -v /home/esantanche/SwProjects/Drupal8RestApiShowbox/:/app -w /app -t -d -p 3000:3000 --name=Drupal8RestApiShowbox nodejsgitbashnpm

The container's name is Drupal8RestApiShowbox.

I use this container for a Sapper/Svelte development environment. There will be a development server running on port 3000. I tell Docker that I want to access that server from the host machine (my computer) on the same port.

The option -t allocates a tty terminal to the container so that it can successfully run bash.

I won't start typing on the container's console straight away. I'll do it later. That's the reason for the presence of the -d option.

When I open a shell on the container, I find myself on the folder /app. This is a folder inside the container, don't confuse it with any folder on the host machine.

When working on the container, I magically find in that folder the content of the host's folder /home/esantanche/SwProjects/Drupal8RestApiShowbox/.

I run commands in the container that alter the content of /app and I see the same changes in /home/esantanche/SwProjects/Drupal8RestApiShowbox/.

After having given this command, the container is running. But I may have to stop it and need to restart it again later.

Starting the container

To start the container I don't use the run Docker command. It would create another container. In this case it would fail because it would find that a container named Drupal8RestApiShowbox already exists.

I just start the container instead.

[esantanche@luna ~]$ sudo docker start Drupal8RestApiShowbox

I can open a bash shell on the running container.

[esantanche@luna ~]$ sudo docker exec -i -t Drupal8RestApiShowbox bash

Installing Sapper/Svelte

I open a console on the container and I find myself in the folder /app.

I run these commands.

npx degit "sveltejs/sapper-template#rollup" sapperapp
cd sapperapp/
npm install
npm run dev

The first one downloads and installs Sapper in the /app folder creating a subfolder named sapper.

Then npm installs the needed packages and runs the development server on port 3000.

I open a browser on http://localhost:3000/ and see the demo page Sapper installs as a default.

Coding time

At this point I'm ready to play and write the demo application.

The code is here: https://github.com/esantanche/drupal8restapishowbox.

Building the application to production

I develop the application on my computer. I can edit the code without having to do it in the container because it lives in the local folder /home/esantanche/SwProjects/Drupal8RestApiShowbox/. The container and its Sapper server can use it because my computer's folder is mapped to the container's one at the path /app.

When I'm happy with the result, I connect to the container and build the application.

[root@2b7a67476c15 sapperapp]# npx sapper build

I have to copy the entire folder /home/esantanche/SwProjects/Drupal8RestApiShowbox/sapperapp/ to my server.

Remember that this folder maps to the folder /app/sapperapp in the container.

On the server, I run a node server process with the command below.

root@FREEDOMANDCOURAGE:/srv/sites/drupal8restapishowbox.emanuelesantanche.com/sapperapp# node __sapper__/build/
Starting server on port 3000

Having built and not exported the application, the ability to render pages on the server is available.

I configure the web server nginx and the DNS records required to serve the application to the internet.

I define a service so that I can start the node server with a simpler command.

root@FREEDOMANDCOURAGE:/etc/systemd/system# service drupal8restapishowbox start

I configure the service for it to start at boot.

The application is live at http://drupal8restapishowbox.emanuelesantanche.com/.

 

Photo by Jan Phoenix on Unsplash
 
Emanuele Santanche