Using the Content API

Lightning 2.1.7 includes a new top-level component: Content API. Its purpose is to provide a very basic server-side framework for building decoupled apps using Lightning as a backend. It has no strong opinions about how the "front-end" of such an application is implemented -- out of the box, it merely provides tools to deliver Drupal entities according to the JSON API specification.

Generally speaking, you can interact with API anonymously in the same way that an anonymous user can interact with a standard Drupal site. So you can do things like get a single piece of content, or a list of content without authenticating. For other actions -- the kind that would normally require you to be logged in to Drupal -- you will need to provide an OAuth access token in the header of your request. Tokens are related to a Drupal user and an OAuth client, which is associated with any number of Drupal user roles. You can obtain a token by making a specific HTTP request for it.

Let's go through some common, generic, use cases. I'll use cURL in my example so that you can easily test them out for yourself.

Getting a list of content

The API endpoints generally follow the following pattern: "/jsonapi/{entity-type}/{bundle}". So if we wanted to get a list of Basic Page content, we could send a GET request to "/jsonapi/node/page":

curl --request GET \
 --url https://example.com/jsonapi/node/page

Which would return something like this:

{
  "data": [
    {
      "type": "node--page",
      "id": "api_test-unpublished-page-content",
      "attributes": {
        "nid": 1,
        "uuid": "api_test-unpublished-page-content",
        "vid": 1,
        "langcode": "en",
        "status": false,
        "title": "Unpublished Page",
        "created": 1502985175,
        "changed": 1502985175,
        "promote": false,
        "sticky": false,
        "revision_timestamp": 1502985175,
        "revision_log": null,
        "revision_translation_affected": true,
        "default_langcode": true,
        "path": null,
        "body": {
          "value": "--TESTING--",
          "format": null,
          "summary": null
        }
      },
      "relationships": {
        "type": {
          "data": {
            "type": "node_type--node_type",
            "id": "8bae5c5c-697d-4b8a-ab22-b72e895a3b24"
          },
          "links": {
            "self": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/relationships/type",
            "related": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/type"
          }
        },
        "uid": {
          "data": {
            "type": "user--user",
            "id": "4d7eb3c7-db6d-4a01-8b3d-7d706d314f87"
          },
          "links": {
            "self": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/relationships/uid",
            "related": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/uid"
          }
        },
        "revision_uid": {
          "data": {
            "type": "user--user",
            "id": "4d7eb3c7-db6d-4a01-8b3d-7d706d314f87"
          },
          "links": {
            "self": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/relationships/revision_uid",
            "related": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/revision_uid"
          }
        },
        "moderation_state": {
          "data": {
            "type": "moderation_state--moderation_state",
            "id": "1a5f02e6-3f14-46a7-a40c-65590c8729a9"
          },
          "links": {
            "self": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/relationships/moderation_state",
            "related": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content/moderation_state"
          }
        },
        "scheduled_update": {
          "data": [
            
          ]
        }
      },
      "links": {
        "self": "https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/jsonapi/node/page/api_test-unpublished-page-content"
      }
    },
...

That's pretty verbose. We could simplify the response by adding the "fields" parameter. In this example, we only want the "title" and "created" fields:

curl --request GET \
 --url https://example.com/jsonapi/node/page\
?fields[node--page]=title,created
# Note that I'm using `[` and `]` here for clarity. These characters need to be
# encoded with `%5B` and `%5D` respectively if you want to actually use these
# examples.

Which would return something like this:

{
  "data": [
    {
      "type": "node--page",
      "id": "0bee8eb7-0f06-4986-9ca0-e340021a0af3",
      "attributes": {
        "title": "A Page",
        "created": 1502985175
      },
      "links": {
        "self": "https://{DOMAIN.COM}/jsonapi/node/page/0bee8eb7-0f06-4986-9ca0-e340021a0af3"
      }
    },
    {
      "type": "node--page",
      "id": "4d7eb3c7-db6d-4a01-8b3d-7d706d314f87",
      "attributes": {
        "title": "Another Page",
        "created": 1502985175
      },
...

Getting a specific piece of content

We can request a specific piece of content by specifying its UUID in the URL:

curl --request GET \
 --url https://example.com/jsonapi/node/page/0bee8eb7-0f06-4986-9ca0-e340021a0af3

 Which would return something like this (but more verbose since we didn't use the "field" parameter):

{
  "data": {
    "type": "node--page",
    "id": "0bee8eb7-0f06-4986-9ca0-e340021a0af3",
    "attributes": {
      "title": "A Page",
      "created": 1502985175
    },
    "links": {
      "self": "https://example.com/jsonapi/node/page/0bee8eb7-0f06-4986-9ca0-e340021a0af3"
    }
  },
  "links": {
    "self": "https://example.com/jsonapi/node/page/0bee8eb7-0f06-4986-9ca0-e340021a0af3?fields%5Bnode--page%5D=title%2Ccreated"
  }
}

Getting a token

You will need to provide an access token for any request that anonymous users are not authorized to execute. Tokens are granted via the "/oauth/token" endpoint, and requests for a token must include a client_id, client_secret, username, and password. OAuth clients inherit the permissions of standard Drupal user roles by selecting one or more roles on the client's configuration form, under "Scopes". A typical setup would involve the following steps:

  1. Create a Drupal role ("/admin/access/roles") with the permissions you want the consuming app to be allowed to perform.
  2. Create a Drupal user ("/admin/people/create") that the API will use and assign that user the role you just created.
  3. Create an OAuth2 client ("/admin/config/people/simple_oauth/oauth2_client/add") and assign it the same role as the user you just created via the Scopes section.

Once that's done, you can use the following to obtain an access token, where:

  • CLIENT_ID = The OAuth2 client UUID, displayed after creation of the client in Step 3 at "/admin/access/clients"
  • SECRET = The "New Secret" you chose when creating the client  in Step 3
  • USERNAME = The Drupal username of the user you created in Step 2
  • PASSWORD = The password you gave the Drupal user in Step 2
curl -X POST -d \
 "grant_type=password\
&client_id={CLIENT_ID}\
&client_secret={SECRET}\
&username={USERNAME}
&password={PASSWORD}"\
 https://example.com/oauth/token

Which should generate a response like this:

{
    "token_type": "Bearer",
    "expires_in": 300,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUz...",
    "refresh_token": "def50200bdb9093a7a6cc837dhcd1..."
}

If you want to give it a try without your own sandbox setup, Headless Lightning has a nightly build deployed to https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com with a client and user preconfigured. So you should be able to use the "/oauth/token" endpoint there to get a valid token to our sandbox if you're curious.

Give it a try! Copy and paste the following into a terminal window:

curl --request POST \
--data "grant_type=password\
&client_id=api_test-oauth2-client\
&client_secret=oursecret\
&username=api-test-user\
&password=admin"\
 https://headlessnightlytfrimmmkug.devcloud.acquia-sites.com/oauth/token

Using a token

Once you have a token, it's easy to get data that anonymous users aren't authorized to access. Just add an Authorize header to your request, like so (replacing {ACCESS_TOKEN} with the access_token value in the /oauth/token response):

--header 'authorization: Bearer {ACCESS_TOKEN}'

So let's say we wanted to get a specific piece of content just like the "Get a specific piece of content" example above. But in this case, the content is unpublished and therefore anonymous users won't be able to access it. Given that the token was acquired:

  1. For an OAuth client that has a scope with the "View unpublished content" permission
  2. For user account that has a role with the same permission

We can successfully make the same request for an unpublished piece of content if we include the token in an authorization header like this:

curl --request GET \
 --header 'authorization: Bearer {ACCESS_TOKEN}'\
 --url https://example.com/jsonapi/node/page/api_test-unpublished-page-content
# Where `api_test-unpublished-page-content` is the UUID of some piece of
# unpublished content

Note how this request is identical to the anonymous request above except that it:

  1. Requests a resource that requires authorization
  2. Includes an "authorization" header

Given the authorization header, Content API will authenticate the request and then authorize it (or not) based on the permissions of the associated client and user.

Creating content

You can create new content by sending a POST request to "jsonapi/{entity-type}/{bundle}". You'll need to include a specific Content-Type header, and most configurations will require Authorization as well since anonymous users usually can't create content. For example:

curl --request POST \
 --data '{"data": {"type": "node--page","attributes": {"title": "Created via JSON API"}}}'\
 --header 'Content-Type: application/vnd.api+json'\
 --header 'authorization: Bearer {ACCESS_TOKEN}'\
 --url https://example.com/jsonapi/node/page

Content vs Configuration Entities

Drupal makes a distinction between Content and Configuration entities. Sometimes content entities are further distinguished as being renderable and/or bundle-able. Content API makes no such distinctions. If your API client/user have permission to interact with an entity, it can do so through the API. That means you can do things like add fields to a content type via the API, or edit a moderation state transition.

Headless Lightning

Everything described here can be done with Lightning. But if you're building a decoupled application, you might want to check out Headless Lightning, which has a few additional features (and a few features removed) which make it more suitable for decoupled applications.

Powered by Drupal Lightning