Kirby REST API via HTTP in JSON format

Hello everyone, I’ve done a few research for JSON API of Kirby and found only a basic API method. I’m a familiar with a WP REST API, and AngularJS. Someone had any experience with Kirby or even build a plugin?

The idea is pretty simple, the url structure might be:

  1. domain.ltd/api — Global data
  • site data
  • first level pages data
  1. domain.ltd/api/page — For pages that have a second level data, like products, blog or portfolio:
  • page data
  • list of subpages with all data (ex. limited to ‘X’ pages)
  1. domain.ltd/api/page/page:1 — If the parent page has more than ‘X’ pages
  • page data
  • list of subpages with all data (ex. limited to ‘X’ pages)

Here’s the brainstorming:

1 Like

Now we have routes so I would use that instead of an invisible page.

I hope someone build it as a plugin like WP REST API.

Actually I found this on GitHub: https://github.com/TimKaechele/Api.plugin, but sadly it doesn’t work anymore…

1 Like

Same here. API’s are very relevant.
I also found the same tutorial, but it’s old now, and I’m new to API’s so I’m not sure.

I wish I could contribute in any way.
An API is very necessary I think.

The tutorial may be old, but it’s still valid, so you could use that as a starting point. Instead of using an invisible page, you can use a route, of course, as rightly pointed out by @jenstornell.

2 Likes

I’ve built one recently, but unfortunately I can’t share it.

I’ve found that instead of a plugin-type solution, it’s best if you explicitly create your objects that the API gets/returns. Sure, you can just json_encode() a page object’s content, but you end up exposing things that you don’t necessarily want exposed.

(See below heading “Another Option” for a really simple approach.)

You could use template files to output JSON, but you’re much better off using Kirby’s router, much more flexible and powerful and much easier to change in the future. And a JSON response isn’t really a template or a view anyway; it’s data.

And with routes, you can apply route filters, like ensuring a user is logged in or has a certain role.

I’ll roughly describe my approach here, these code samples are untested but should get you going. Note that I’ve taken mine much farther, using Composer libraries and my own classes for handling the routes.

You’ll probably have to use [Field]->toString() and [Field]->value() and [Field]->int() when serializing your objects, since Kirby utilizes built-in __toString() for output from templates and snippets.

1. Create a plugin for your routes

// site/plugins/api.php

$prefix = "api/v1";

kirby()->routes([
    
    # GET events
    [
        'method' => 'GET',
        'pattern' => "{$prefix}/events",
        'action' => function() {
            // I like to use my own controllers instead of doing the work here
            // return (new MyApp\APIController)->getEvents()

            // get the Event pages
            $collection = pages()->find('events')->children()->visible();

            // You could just serialize the collection, but
            // you may not want to expose all that data
            // return response::json($collection->toJson());

            // Transform them for API delivery
            $data = $collection->map(function ($event) {
                // `serialize()` is not built in, see page models below
                return $event->serialize();
            });

            return response::json($data);
        }
    ]

]);

2. Create a page model

This is where you define serialize(), shown above.

You could create your data objects right in the api.php plugin, but I found it’s better to encapsulate that in the Page itself. Let each Page object manage its own serialization for API delivery.

I actually create a model for every page type in my site, each extending my own BasePage class, where a default serialize() function is included, which just spits out url, title, text, and some other basic fields. This way I’m not forced to define serialize() on every single Page type.

// site/models/event.php

class EventPage extends Page
{
    public function serialize()
    {
        return [
            'uid'     => $this->uid(),
            'url'     => $this->url(),
            'title'   => $this->title()->html()->toString(),
            'text'    => $this->text()->kirbytext(),
            'special' => $this->someSpecialFunction(),
        ];
    }
    public function someSpecialFunction()
    {
        return (int) ($this->someField()->int() + $this->someOtherField()->int());
    }
}

This is just a basic intro… Again, I’ve found it’s best to take the time to explicitly build up your own API. I considered writing a distributable plugin for getting a basic API, but the reality is that every API has different needs.

WordPress’s database structure is standardized, even across content types (post_types), so a generic, one-size fits all REST API makes sense. But in Kirby, we store all sorts of content in so-called “Pages”.

I do think there’s room for an API plugin, but it’d require so many ways of customizing, so many hooks into the system, that I think in the end it’s less work to just build it yourself.

Another option

If you’re just looking to expose all page’s public data via an api, it may make more sense to simply append json to the existing routes. For example, you could:

/pages/page-1/json
/pages/page-2/json
/events/2016/json

You’d still have to create a router and route each URI, so you’d essentially be recreating most of kirby’s default/built-in routing.

But if you did this (or something similar):

/pages/page-1/?format=json
/events/2016/?format=json

You could write a plugin that checks for get('format') and stops template execution, returning JSON instead.

But often when a plugin is called, kirby’s router isn’t done; so your plugin doesn’t know which page is being requested yet. You’re probably better off using page controllers.

// site/controllers/events.php

return function ($site, $pages, $page)
{
    if (get('format') == 'json')
    {
        $data = [
            'uid'   => $page->uid(),
            'title' => $page->title()->toString(),
            'text'  => (string) $page->kirbytext(),
        ];

        die(response::json($data, 200));
    }
}

Hope this helps!

8 Likes

Great approach!

Your explanations made me think about my “different content representations” feature I created in January. Maybe this could simplify creating a JSON response in the future. But well, it has to be tested and merged first. :wink:

4 Likes

I tried to implement the API plugin and page model as you described under 1. and 2. but I get a 404 error. Does that have anything to do with plugins apparently being loaded before the router is initialized, as mentionend in another post?

Note that I’ve changed pages() to $pages in site/plugins/api.php, which I assume was a typo?

Any idea what I might be missing?

Nope, pages() is correct. $pages won’t actually work here, because global variables are not available in functions.

I see, changed it back to pages(). That didn’t solve the error unfortunately.

I just tested the code in a test installation. If I request /api/v1/events, I get the result of the route back.

Please check the following:

  • Is the 404 a Kirby error or from your web server (Apache)?
  • Do you use the latest Kirby version v2.2.3?
  • Do you use PHP 5.4+?
  • Please verify that the file site/plugins/api.php is there and starts with <?php.

Is the 404 a Kirby error or from your web server (Apache)?

I’m trying to log the data with

$.getJSON('/api/v1/blog', function(r) {
  console.log(r);
});

which gives me the following error in the browser console:
GET http://localhost:3000/api/v1/blog 404 (Not Found)

so I guess is it’s Apache related.

Do you use the latest Kirby version v2.2.3?

Yes

Do you use PHP 5.4+?

5.6.10

Please verify that the file site/plugins/api.php is there and starts with <?php.

This is my site/plugins/api.php with blog posts instead of events, but the structure is the same:

<?php

$prefix = 'api/v1';

kirby()->routes([

  # GET blog articles
  [
    'method' => 'GET',
    'pattern' => '{$prefix}/blog',
    'action' => function() {
      // get the article pages
      $collection = pages()->find('blog')->children()->visible();

      // Transform them for API delivery
      $data = $collection->map(function($article) {
        return $article->serialize();
      });

      return response::json($data);
    }
  ]

]);

And heres the page model in site/models/article.php:

<?php

class ArticlePage extends Page
{
  public function serialize()
  {
    return [
      'uid'     => $this->uid(),
      'url'     => $this->url(),
      'title'   => $this->title()->html()->toString(),
      'text'    => $this->text()->kirbytext(),
      'special' => $this->someSpecialFunction(),
    ];
  }
  public function someSpecialFunction()
  {
    return (int) ($this->someField()->int() + $this->someOtherField()->int());
  }
}

Please try accessing the URL directly in your browser. What does it look like?

What kind of server is running on port 3000?

Okay I might have found the issue: I’ve used single quotes where @jevets used double quotes. But now I get a 500 Internal Server Error when I’m trying to access http://localhost:3000/api/v1/blog in the browser.

I’m using MAMP with the vhost set up as a proxy in BrowserSync.

Try casting 'text' to a string:

  public function serialize()
  {
    return [
      'uid'     => $this->uid(),
      'url'     => $this->url(),
      'title'   => $this->title()->html()->toString(),
      'text'    => $this->text()->kirbytext()->toString(),
      // you don't need this, was just an example to show how you could handle custom stuff
      // 'special' => $this->someSpecialFunction(),
    ];
  }

The code I’d originally posted was never tested, so I’d expect some issues. Was really just trying to illustrate the concept.

(BTW, I am working on a plugin to make building RESTful APIs easier, though there will still be some manual work involved.)

should be in double quotes for interpolation

'pattern' => "{$prefix}/blog",

You could concatenate, if you prefer

'pattern' => $prefix . '/blog',

Dumping the routes can also be hugely helpful when getting routes working:

var_dump( kirby()->routes() );
die;