Building API Endpoints

Building API Endpoints

Learn how to build a simple API using CMS Pages.

View the routing article if you prefer to define API routes using PHP instead.

When working with client-side frameworks such as Vue.js or React, it will be necessary to consume server-side APIs. These can be defined using your theme where each page represents an API endpoint.

The page represents a transformation layer that sits between your CMS Components and the JSON responses that are returned to your application. Most objects, such as models and collections, support JSON serialization and can be returned directly as a response.

# Sending a Response

In the simplest form, an API resource can be composed by returning a Twig variable using the response() Twig function. This function overrides the page contents and returns a custom response to the browser.

url = "/api/foobar"
{% do response({ foo: 'bar' }) %}

The above call will return a response with a application/json content type.

{ "foo": "bar" }

In most cases you will be converting component variables to a response.

{% do response({
    id: post.id,
    title: post.title,
    email: post.author.email,
    created_at: post.created_at,
    updated_at: post.updated_at
}) %}

# Collections

The collect()Twig function builds a collection for a response. The push method is used to push items on to the collection and it can be used to customize each result.

{% set result = collect() %}

{% for post in posts %}
    {% do result.push({
        id: post.id,
        title: post.title,
        email: post.author.email,
        created_at: post.created_at,
        updated_at: post.updated_at
    }) %}
{% endfor %}

{% do response(result) %}

# Conditions

All Twig conditions can be used in the markup to affect the response and the last response call is the one that will be sent to the browser.

# Checking the HTTP Method

Use the this.request.method Twig property to check the request method.

{% if this.request.method == 'GET' %}
    <!-- Do GET Logic -->
{% else %}
    <!-- Method Unsupported -->
{% endif %}

# Aborting the Request

The abort() Twig function can be used to abort the request with a 404 response.

{% if post %}
    {% do response(post) %}
{% else %}
    {% do abort(404) %}
{% endif %}

# Working with Pages and Layouts

Since the API is defined in the Markup section of the page or layout, all components and life-cycle events are available.

# Using Layouts as Middleware

Middleware allows you to apply common logic to multiple endpoints, such as checking authentication or throttling requests. A CMS layout with priority mode can be used to apply logic to multiple pages and the layout logic executes before the page logic.

Remember to include the {% page %} Twig tag so the page logic is included. For example, a layout named api.htm can have any conditional logic.

description = "API Authentication"
is_priority = 1
{% if someCondition %}
    {% page %}
{% else %}
    {% do response({ message: 'Condition not met' }, 400) %}
{% endif %}

Every page that uses the layout will have the conditions of the layout applied.

layout = "api"
{% do response({ success: true }) %}

Always use priority mode in the layout to ensure the layout contents run first.

# Calling AJAX Handlers

In some cases you may wish to call an AJAX handler of a component or inside the page. This is possible using the ajaxHandler() Twig function.

url = "/api/signin

[account]
{% set result = ajaxHandler('onSignin') %}

{% if result.error %}
    {% do response({ message: 'Login Failed' }, 401) %}
{% else %}
    {% do response({ success: true }) %}
{% endif %}

You may also call a handler and pass it directly as a response. The response includes variables set on the page and array values returned by the function.

{% do response(ajaxHandler('onSubmitPost')) %}

It will also handle redirects automatically. See the Twig function article for more details.

# Working with Resources

When working with models and collections, it is recommended to return data wrapped in the data attribute. Wrapping the response provides a consistent interface.

# Models & Collections

Returning a model resource.

url = "/api/blog/post/:slug"

[section post]
handle = "Blog\Post"
identifier = "slug"
{% if post %}
    {% do response({
        data: post
    }) %}
{% else %}
    {% do abort(404) %}
{% endif %}

Returning a collection resource.

url = "/api/blog/posts"

[collection posts]
handle = "Blog\Post"
{% do response({
    data: posts
}) %}

# Pagination

When responding with a paginated collection, it is recommended to use the pager() Twig function to construct the response using the links and meta attributes.

{% set posts = blog.paginate(3) %}

{% set pager = pager(posts) %}

{% do response({
    data: posts,
    links: pager.links,
    meta: pager.meta
}) %}

The above will output the following JSON format.

{
    "data": {},
    "links": {
        "first": "https://yoursite.tld/api/blog/posts?page=1",
        "last": "https://yoursite.tld/api/blog/posts?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "path": "https://yoursite.tld/api/blog/posts",
        "per_page": 3,
        "total": 2,
        "current_page": 1,
        "last_page": 1,
        "from": 1,
        "to": 2
    }
}

# Usage Examples

These are some practical examples of how snippets can be used.

# Returning Users with Avatar Thumbnails

The following example sets the users variable to all the users found in the User plugin (opens new window). The avatar relationship is eager loaded after the fact and then the avatar_thumb attribute is set as a thumb URL for each user if an avatar is found.

## pages/api/users.htm
url = "/api/users"
function onStart()
{
    $this['users'] = \RainLab\User\Models\User::all();
}
{# Load up the avatar relation #}
{% do users.load('avatar') %}

{# Set the 'avatar_thumb' attribute on each user #}
{% for user in users %}
    {% do user.setAttribute(
        'avatar_thumb',
        user.avatar.getThumbUrl(100, 100, {mode: 'crop'})|default(null)
    ) %}
{% endfor %}

{# Respond with the user #}
{% do response({
    data: users
}) %}

# See Also