Creating an API with Twig

Posted by Jake Dohm on 1 April, 2019

Creating an API with Twig illustration

Creating an API can be tricky. You have to install the Element API plugin, configure it and set up your endpoints. Not to mention that if you’re not as familiar with PHP it can be difficult to figure out how to query for what you need!

On the other hand Twig APIs are easy to create and built in a language you already know! This type of API doesn’t work for every scenario, and it can feel a bit hacky’ at times, but for simple APIs Twig is a great solution.

Basic API

So, how should you go about creating an API in Twig? Since Craft allows users to hit Twig templates directly by going to their file path, all you need to do is create a Twig file.

Basic Example

For a super simple example you can create a Twig file templates/api/entries.json with the following contents:

{# templates/api/entries.json #}

{% set entries = craft.entries.all() %}

{{ entries | json_encode | raw }}

In this example we’re querying for all our entries and encoding the results of that query to be JSON, using raw to allow HTML characters.

This is probably an oversimplified example, because this method doesn’t return all of the fields on each entry. Fields like assets’ and entries’ have to be queried for, and won’t be returned from this basic API. But it’s a great starting point, and can work for very basic scenarios.

Note: This is a JSON API, but it could be any type. You could manually build out XML or another data type (and I have), but since JSON is the most popular data type for APIs we’ll be returning JSON.

API with Params

In our basic example the API grabs all entries, but that isn’t a very common use-case. A much more common example is getting all entries from a certain section. In the following example we’ll add the ability to query for entries in a certain section, and limit how many entries we return.

{# templates/api/entries.json #}

{% set request = craft.app.request %}

{% set section = request.getQueryParam('section') %}
{% set limit = request.getQueryParams('limit') %}

{% set entries = craft.entries.section(section).limit(limit).all() %}

{{ entries | json_encode | raw }}

Alright, let’s look at what we did above:

  • We’re defining section’ and limit’ variables based on query params. This means that if someone visits example.com/api/entries.json?section=blog&limit=5 then section’ will be set to blog’ and limit’ will be set to 5.
  • We then pass those variables into our entries’ query, and return the entries’ query.

Note: When you call getQueryParam if a param with that name doesn’t exist, it returns null. This is convenient because it means if you hit our endpoint without a section’ or limit’ param the API will still work and return all entries with no limit.

Adding Authentication

Sometimes the data you want to expose shouldn’t be available to just anyone. In this case we want to add a level of authentication to make sure the user is logged in.

Since the request to our Twig endpoint will be made in the same session as our normal pages we can use the currentUser variable to determine whether the user making the request is logged in or not.

{% if currentUser %}
  {% set request = craft.app.request %}

  {% set section = request.getQueryParam('section') %}
  {% set limit = request.getQueryParam('limit') %}

  {% set entries = craft.entries.section(section).limit(limit).all() %}

  {{ entries | json_encode | raw }}
{% else %}  
  {% exit 401 %}
{% endif %}

In the above code we check to see if there is a currentUser. If so we return the entries like normal; if not we throw an error. You can customize this error response, but I’m throwing a 401 error’ meaning the request was unauthorized.

Note: This is a very crude authorization system with very mediocre error’ reporting (throwing a 401 template). For something robust I would lean on a more full-featured API system.

Real World Example (with SmartMap)

Finally, here’s a slightly more realistic example I’ve previously implemented in production!

Below is a Twig API that takes two parameters: a search value and a limit. It will use SmartMap (todo: link) to search for locations nearest to the search string, order them by distance, and return them to us in JSON.

{# Get our paramater values #}
{% set search = craft.app.request.getQueryParam('search') %}
{% set limit = craft.app.request.getQueryParam('limit') %}

{# Build our query, based on parameters #}
{% set locations = craft.entries.mapAddress({ target: search }).orderBy('distance').limit(limit).all() %}

{# Create an empty array to fill with our data #}
{% set locationsArray = [] %}

{# Loop over our items and fill our empty array with the necessary data #}
{% for location in locations %}

  {# Choose the properties that we need from our API #}
  {% set locationsArray = locationsArray | merge([{
    title: location.title,
    url: location.url
  }]) %}

{% endfor %}

{# Output JSON encoded array of locations #}
{{ locationsArray | json_encode | raw }}

I like this example because it shows all the steps we generally need to build the necessary data:

  1. Get our parameters (if any)
  2. Build our query based on parameters
  3. Create an empty object to fill
  4. Loop over our items and fill our empty array with the necessary data
  5. Output our data as JSON

Conclusion

Does this replace the Element API plugin, Craft QL, or creating your own API endpoints? No, definitely not. Those are all powerful tools that do more/​better than this method. However, this method is great for a simple API, or when you’ve already figured out how to query for something in Twig and don’t want to convert that code to PHP. Use it in good health at your discretion!