ExpressionEngine and Headless

Headless is all the rage, and for good reason. People and devices consume content in a variety of ways that have needed a different approach to the CMS as a single platform to handle both the managing of content and displaying the content. The idea of a headless CMS focuses the responsibility of the CMS to only administrating content, while the presentation of content is left to a different stack of tools. The presentation can also benefit from static CDN caching for scaling, and increased security. We are not talking about just blog posts, but also data like the current temperature outside, rocket launch times, and infection rates.

In the world of headless, ExpressionEngine is considered a hybrid CMS. It is able to deliver on many headless concepts, so the final answer depends on which part of the definition is the most useful for your case. Let’s first cover what ExpressionEngine does well around the scope of headless out of the box.

What ExpressionEngine Can Do

ExpressionEngine excels at content management through user-defined custom fields that are grouped into channels of content. Said another way, content can be broken into highly structured fields that fit the needs of the site. It has robust roles & permission management for users, and a template engine able to output text in most formats, from JSON to HTML and XML. ExpressionEngine can also be wildly extended by add-on modules. All of this is managed by an intuitive control panel.

ee entry editing screen

What ExpressionEngine is Not

ExpressionEngine is not offered as a SaaS, or built with microservices, and is not currently offered as a fully maintained cloud option. ExpressionEngine is a traditional monolithic PHP application that must be downloaded and installed on a LAMP-like stack. Like most CMS’s in this category, the core can be auto-updated with a single click. It also does not have GraphQL, and editing entries through the API takes a little bit of extra work.

Out of the Box

Most APIs for a headless CMS are typically going to return JSON, and this is absolutely possible in ExpressionEngine! To create a JSON-formatted template, simply create a new template, select the type of javascript, and create a tag loop to output entry data. Since templates are independent of each other, it is also possible to run both a standard HTML website alongside various API endpoints in the same ExpressionEngine install.

ee template editor

Limitations

No rate limits

Since these behave like normal web pages, there is not a way to limit page requests per API key.

Lack of access control with something like tokens

API keys and tokens in the header of a request to control access or enforce limits are not supported.

GET is the only request type

Requests that modify data such as POST, PATCH, and DELETE would not be supported out of the box. See further down the article for more information.

GET Example

To display a list of upcoming events at, say, a conference center, first create a channel to hold the event information such as the title, event date…etc. Conference rooms in the facility would have their own channel and contain information like hall capacity, name, and photos and then be related to the event. This structure can then be outputted in templates:

Example of an HTML template at /events/item/my-event

{exp:channel:entries channel="events"}

  <!-- Event Info -->
  <h1>{title}</h1>
  <time>{entry_date format="%m/%d/%Y"}</time>
  <p>{description_custom_field}</p>

  <!--- Room Relationship -->
  {conference_room}
    <h3>Room: {conference_room:title}</h3>
    <p>Capacity: {conference_room:size}</p>
  {conference_room}

{/exp:channel:entries}

Example of the same data in JSON at /api/events/my-event

{exp:http_header content_type="application/json"}
[
  {exp:channel:entries channel="events" backspace=’1’}
  {
    // Event Info:
    'title' : '{title}',
    'entry_id' : {entry_id},
    'description' : ' {description_custom_field}',

    // Room Relationship:
    'conference_room' : [
      {conference_room backspace='1'}
      {
        'room_title' : '{conference_room:title}',
        'size' : '{conference_room:size}'
      },{/conference_room}
    ]

  },{/exp:channel:entries}
]

Creating Better Queries

The vanilla channel entries loop is a bit cumbersome to fine-tune queries using the URL like a typical REST API. To get more control, URL query parameters can be used through the first-party add-on Low Search. They can be any of the standard fields such as date, status, or title plus all custom fields, and relationships.

The most common use for this approach is updating a page with search results using asynchronous javascript (AJAX). Andy McCormick wrote a series on this. While in Andy’s series the templates returned HTML, it can be easily changed to JSON:

Entries Template /group/entries

{exp:http_header content_type="application/json"}
[
  {exp:low_search:results query="{segment_3}" backspace="1"}
    {
      'title' : '{title}',
      'entry_id' : {entry_id},
      'custom_field_1' : '{custom_field_1}',

    },{/exp:low_search:results}
]

Static HTML File:

const request_url = '/group/entries?status=open&channel=events&limit=10';

const entries  = await fetch(request_url).then(response => response.json());

entries.forEach( entry => {

  console.log(entry.title);
  console.log(entry.entry_id);
  console.log(entry.custom_field_1);

});

Extending with Add-ons

ExpressionEngine can be extended with Add-ons written by 3rd party developers through the ExpressionEngine Store, or can be custom-written per-project. Here are just a few to check out:

Bones by TripleNERDScore

https://expressionengine.com/add-ons/bones

Bones combines both the Low Search query URL feature, plus gives a full output of entry data in a predictable format. This skips a lot of custom template authoring. Data can be returned as JSON through an action URL (see below), or template. Bones also provides the ability to require an API key in order to access data, adding to security. Like the Low Search approach, data queries are made through the URL parameter. Bones also helps with counting objects, and better error handling than a standard template.

Webservice by Reinos

https://expressionengine.com/add-ons/reinos-webservice

This add-on provides a highly customized web service for ExpressionEngine that includes GET and POST processes.

Custom Add-ons

If none of these options suit your needs, you can create your own custom add-on. Add-ons can tap into control panel events such as when entries are updated, or someone logs in, to have their own custom logic and endpoints for data.

When building an ExpressionEngine module there is a service called Action ID, or “ACT”. A module method can be run when a specific URL is requested. This can be the start of your API and can accept GET as well as POST and other requests to update ExpressionEngine. For example, by visiting the URL below, the function act_action would be called.

https://example.com/index.php?ACT=23&action=get_entries

// On install, ExpressionEngine can
// create a new ACT ID to the method name of your choice.
function act_action()
{

  if (ee()->input->post('action') === 'get_entries')
  {

    $entries = ee('Model')->get('ChannelEntry')->all();

    return json_encode($entries);

  }

}

Modifying Entries

Creating and updating entries in ExpressionEngine will take a little bit of work, and require building a Module add-on.

When installing an add-on’s ACT action, setting the csrf_exempt to 1 will disable the token check as shown below. At this point any data can be submitted to your method, so be careful how this is used.

Also to note, updating Grid and Fluid fields take additional steps to be updated and are beyond the scope of this writeup.

Within the add-ons’s upd.myaddon.php file:

$data = array(
    'class'     => 'MyClass' ,
    'method'    => 'my_act_method',
    'csrf_exempt' => 1 // Important, use with care.
);
ee()->db->insert('actions', $data);

From here, the my_method function can alter data in ExpressionEngine. For example, to update the title of an entry we could do something like the following:

POST Request:

curl -d 'title=NewTitle!&entry_id=5' -X POST https://example.com/index.php?ACT=23

mod.myaddon.php :

function my_act_method()
{

    // Get fields from POST using sanitized methods:
    $entry_id  = ee()->input->post('entry_id');
    $new_title = ee()->input->post('title');

    // Use ExpressionEngine models to get an entry.
    $entry = ee('Model')->get('ChannelEntry', $entry_id)->first(); 
    $entry->title = $new_title; // Change the title.
    $entry->save(); // Save changes.

}

Final Thoughts

Using ExpressionEngine to power a headless application is absolutely possible, with some limitations. ExpressionEngine excels at content management while providing the possibility for a hybrid of standard HTML and API templates in the same open-source system. It can deliver that data in numerous ways. ExpressionEngine can also be extended with existing or custom add-ons to build more functionality where the native options are limited. Where ExpressionEngine may not fit is if you are looking for a fully-hosted and maintained CMS on a monthly or annual fee with a little extendability, or if a lot of data is being updated outside of the ExpressionEngine control panel.

Blair Liikala's avatar
Blair Liikala

Been using EE for awhile.

Comments 0

Be the first to comment!