Skip to content

Building a REST API for use via AJAX

jbroadway edited this page Nov 29, 2011 · 7 revisions

NOTE: See the new RESTful APIs page for the new and much-improved RESTful Elefant API.

Let's make a generic API based on the Mytable model created on the Database API and models page. To start, create a new app:

cd /path/to/site
./conf/elefant build-app api

Preventing layout output

Open the apps/api/handlers/index.php file and add the following to the top:

<?php

$page->layout = false;

?>

Any request sent to /api or /api/* will route to this handler and we can simply use echo json_encode() to format our output.

Versioning

It's a good idea to version your API if you're exposing it to the public, since you may not have control over updates to 3rd-party clients using it. To do this, we simply create a handler for each new version named v1.php, v2.php, etc. Then we can adjust our main handler to refer to the most current version like this:

<?php

$page->layout = false;

$latest = 'v1';

header ('Location: /api/' . $latest);
exit;

?>

Now whenever someone accesses our API, it will be at /api/v1/* or /api/v2/* making it easy to provide backwards compatibility. Now let's implement our API at v1.php.

Basic structure

There are two ways we can implement our API: In one file with a switch statement, or separated into one file per method. We'll start with one file, then look at how we can split methods into separate handlers after. Note that you can use a switch for most handlers, and split only some into separate handlers depending on what works best in your case.

In v1.php:

<?php

$page->layout = false;

switch ($this->params[0]) {
    case 'all':
        $m = new Mytable ();
        $out = $m->fetch_orig ();
        break;
    case 'item':
        $m = new Mytable ($this->params[1]);
        $out = $m->orig ();
        break;
}

echo json_encode ($out);

?>

We now have two API methods we can use to retrieve data:

/api/v1/all

Retrieve all items from mytable.

/api/v1/item/123

Retrieve an individual item from mytable.

Error handling

If you want to handle errors gracefully, you might want to create a response wrapper with success, data, and error properties like this:

<?php

$page->layout = false;

$error = false;

switch ($this->params[0]) {
    case 'all':
        $m = new Mytable ();
        $out = $m->fetch_orig ();
        if (! $out) {
            $error = $m->error;
        }
        break;
    case 'item':
        $m = new Mytable ($this->params[1]);
        if ($m->error) {
            $error = $m->error;
            break;
        }
        $out = $m->orig ();
        break;
}

$res = new StdClass;
if ($error) {
    $res->success = false;
    $res->error = $error;
} else {
    $res->success = true;
    $res->data = $out;
}
echo json_encode ($res);

?>

Now all of our responses will take the form:

{"success":true,"data":[data]}

Or on error:

{"success":false,"error":"Error message"}

Separating handlers

As your API grows, you may want to move calls from one growing switch statement into their own handlers. For example, say we want to add an /item/add method to our API. We can do that by creating a separate handler based on our original in apps/api/handlers/v1/item/add.php like this:

<?php

$page->layout = false;

$error = false;

$m = new Mytable ($_POST);
if (! $m->put ()) {
    $error = $m->error;
}

$res = new StdClass;
if ($error) {
    $res->success = false;
    $res->error = $error;
} else {
    $res->success = true;
}
echo json_encode ($res);

?>

As you can see, this allows us to eliminate the switch altogether and simplify each handler because we already know the URL matches /api/v1/item/add.

Note that we could also aggregate groups of methods into their own switch by creating a handler like apps/api/handlers/v1/item.php and anything matching /api/v1/item/* will now go there.

PUT and DELETE support

You'll need to configure your web server to send PUT and DELETE requests to PHP, which Apache doesn't do by default. Alternately, you can simply use the X-HTTP-Method-Override HTTP header to specify the real request method. Elefant has two convenience methods to help here:

<?php

// checks X-HTTP-Method-Override or the real request method
$real_request_method = $this->request_method ();

?>

And if the request method is PUT, you can fetch the PUT data from stdin via:

<?php

if ($this->request_method () == 'PUT') {
    $data = $this->get_put_data ();
}

?>

Input validation

In the last example, we simply added a new item without checking any of the $_POST data. Obviously in a real-world example we would need to validate that first. We can use lib/Form.php to help with that.

<?php

$form = new Form ('post', 'api/v1-item-add');
$form->verify_referrer = false;
$form->verify_csrf = false;
if (! $form->submit ()) {
    $error = $form->error;
    if (count ($form->failed) > 0) {
        $error .= ' (' . join (', ', $form->failed) . ')';
    }
}

?>

This will verify both the fields, and the request method. If it's an open API for 3rd-party clients, then we do want to disable the referrer validation however. Next we simply define an INI file for our validation rules and save it to apps/api/forms/v1-item-add.php. The one from Forms and input validation should work here as well since we're using the same schema.

If we just need a single validation done, a full INI file might be overkill. In that case we can say:

<?php

if (! Form::verify_value ($_GET['id'], 'int')) {
    $error = 'ID must be an integer.';
}

?>

Authentication

If you just want an HTTP Basic wrapper around your API, you can add the following code to the top of your handlers:

<?php

$page->layout = false;

if (! simple_auth ()) {
    $res = new StdClass;
    $res->success = false;
    $res->error = 'Authorization required.';
    echo json_encode ($res);
    return;
}

?>

See the Custom user authentication page for info on adding your own authentication mechanism to your API.

Clone this wiki locally