Building a REST-based API in CakePHP
I'm so glad I had a chance to work at MT, where I really understood why a SOA rocked. Reading posts like this also drilled into me the value of a platform.
Of course, applying this concept into the type of clients I work for is difficult. The cost to building out a platform is high, and the returns are negligible in the early days.
With most of my projects, the idea is to get quickly to market, which means our toolset differs a lot from the ones you'd use to build something more solid. That's not to say that what we build is crap - it's not. It's just not geared to be the basis of a platform.
However, the great thing about building an API is eventually you can swap out the underlying technology for something more robust, as long as the endpoints stay the same. Love it.
In any case, we're building an iPhone app, which requires a data transit mechanism. Thought this would be an excellent place to start building out the beginnings of an API.
And boy, did we rush through the v1 - we got it build over a span of about 3 weeks with CakePHP, and here are some lessons learned:
(1) CakePHP has all the inner workings to support a REST-based API. If you want clean URLs, use Routes to do so. Our routes config looks something like this:
Router::connect('/api/users/authtoken', array('controller' => 'api', 'action' => 'users_authtoken', '[method]' => 'GET')); Router::connect('/api/users/current/challenges', array('controller' => 'api', 'action' => 'users_challenges', '[method]' => 'GET')); Router::connect('/api/users/current', array('controller' => 'api', 'action' => 'users_current', '[method]' => 'GET')); Router::connect('/api/users/current/picks', array('controller' => 'api', 'action' => 'users_current_picks', '[method]' => 'GET')); Router::connect('/api/users/challenges', array('controller' => 'api', 'action' => 'users_challenges', '[method]' => 'GET')); Router::connect('/api/challenges/:id/users', array('controller' => 'api', 'action' => 'challenges_users', '[method]' => 'GET'), array('pass' => array('id'), 'id' => '[0-9]+', 'users' => null));
(2) In the early iterations of the API, we got super ambitious and decided to support both XML and JSON. Don't bother. JSON works awesome by itself. Although we eventually abandoned support for XML, we were stuck with API endpoints which had to end in .json to render JSON. The v2 of the API (which we're working on), puts this into the API's beforeFilter() method:
$this->RequestHandler->setContent('json', 'application/json'); $this->layout = 'ajax'; $this->header('Content-Type: application/json');
Everything is returned in JSON - yay!
(3) We were REALLY lazy in the first iteration of the site... so much so that we decided basically to emit the raw data structures coming from the ORM out to the user. That means our views were all the same: json_encode($json), but that means that we weren't able to manipulate a lot of the data before hitting the iPhone layer, which meant we were duplicating a lot of the view-logic in both places.
One example of this: our user model contains a single field: avatar. This contains the filename of the the usericon for that user. However, instead of storing an absolute path, we simply store the filename. A view helper takes that filename and generates/returns a valid usericon string.
By passing the data from the model directly out to the views, we weren't able to catch this.
Not only this, but unless you wrote data suppression methods (which we stupidly had to do), you end up emitting the whole data models to the API. Talk about overkill.
The v2 of the API development has been focused on cleaning up the outputs, so the primary thing we did was write serializers for each data model. This time we relied less on magic: we are writing a serializer helper object that'll handle each data object type and modify the values accordingly and be sure the data models are consistent. (Depending on how you use containers, you'll emit different data structures out as well).
I can't stress the importance of documentation - we didn't do very good documentation first time around, and although the APIs were mostly self-explanatory, the lack of having to write through the documentation created a lot of inconsistencies throughout a lot of the endpoints. This time, I sat down and wrote the documentation in parallel with the views for the JSON objects, and they are a LOT more consistent (much to the delight of the iPhone dev team).
Leaky abstractions occur everywhere - but I'm convinced writing your documentation before (or in parallel) with your code helps nail down the abstractions that are prevalent in user stories.
One area that I'm struggling with - the API is SLOW. Even after cutting down the size of most API calls in half (in terms of data sent), we're still seeing response times in the 600 - 700ms range. That's CakePHP's magical autoassociation of models. I've done my best to only load models on-demand for each API call, but we will need to eventually dive into our models and start unbinding a lot of them (or only binding them when necessary).
What's great about the API is that it's a great investment - if the killer app (the website) does well, these guys are gonna get the beginning of a great technical platform to boot. Of course, at some point we want to move the front-end on top of the API, but I reckon we can do that incrementally over time...