Rolling out an API for your cakePHP app Part 1: The Problems

Recently I've put a lot of work into an API solution for a cakePHP app which addresses some of the architectural issues of creating an API in a cakePHP project. To start with I threw together a nice simple API as a proof of concept and to showcase the ease at which an API can be created quickly using the RequestHandler component. While that solution was fine and incredibly quick to whip up, there are some issues when it comes time to extend the API, the biggest of which is versioning.

This API solution is still a work in progress, there are several ideas I want to poach from other APIs, including a better authentication method possibly using oAuth. The API solution presented here is basically an architectural blueprint to avoid versioning problems, its not a complete API solution, but the skeleton for one. I'm just writing it up now while its fresh in my mind.

A Basic API approach

To create a basic API I took a similar approach to admin routing:

  • Route API URLs with an API prefix
  • Have corresponding api_ prefixed methods in controllers
  • Use the RequestHandler to render file extensions such as .xml and .json
  • Incorporate some kind of authentication using an API key or similar

Going through the code for my basic API approach, first there is the routing:

Router::connect('/api/:controller/:action/*', array('prefix' => 'api', 'api' => true));

Pretty simple, essentially the same as admin routing. So for an example URL:

http://yoursite.com/api/posts/get.xml

Its going to ask for the api_get() method in the posts controller. Using the RequestHandler component in that controller means we can store the xml and json views separately in nicely demarcated folders: /views/posts/xml/get.ctp and /views/posts/json/get.ctp

I'm concentrating on the architecture of the API in these posts but so as not to completely ignore authentication, a basic authentication I have used in the app_controller beforeFilter() method checks if the API is being accessed and uses the apikey passed in the GET variable of the same name to authenticate the user and provide access to the API data being requested.

//In the beforeFilter() of app_controller
if (isset($this->params['prefix']) && $this->params['prefix'] == 'api') {

    //Authenticate a user using api key
    if (isset($this->params['url']['apikey']) && $this->authApi($this->params['url']['apikey'])) {
        $this->Auth->allow($this->action);
    }
    else {
        $this->cakeError('apiRejection');
    }
}
else {
    //Do your usual Auth component authentication
}

/**
 * Helper function to authenticate a user given their api key.
 *
 * @param $apiKey String API key for the request
 * @return Boolean
 */
private function authApi($apiKey = null) {

    if ($apiKey == null) {
        return false;
    }

    $user = $this->User->find('first', array(
        'conditions'=>array('User.apiKey'=>$apiKey),
        'contain'=>false
    ));
    return $this->Auth->login($user['User']['id']);
}

The Problem with this Approach

While its a great approach to get up and running quickly, the problem I faced was: what happens when we want to roll out API version 0.2? Say we changed the URLs for version 0.2 to:

http://yoursite.com/0.2/posts/get.xml

We could add some routing rules to accommodate:

Router::connect('/0.2/:controller/:action/*', array('prefix' => 'api_0_2', 'api_0_2' => true));

But now we need more methods in each controller starting with the prefix api_0_2_ and more views for each, unless we render the old views for the 0.1 version. But it gets messy especially if the API spans many controllers. Upgrading the API requires a lot of code creation and/or duplication, there are lots of methods littered through your controllers that are hard to manage and the views get to be a mess as well.

In short it was too messy and we wanted a way to keep the API methods contained in a single class that could be extended upon for new API versions. I also wanted a nice folder structure for views and some indication of controller specific API methods inside the controller.