Craft 3 + RESTful API

Nate IlerNate Iler

Nate Iler

We’ve been working with Craft 3 since the developer preview release last summer. We decided to poke around with the new Yii2 RESTful web services and before we knew it, we had a fully functional RESTful API for Craft 3. Compared to Element API for Craft 2, the Yii2 RESTful framework is verb driven and supports full CRUD (create, read, update, delete) operations. Our approach is aimed at addressing the following:

  1. Craft Inheritance. Inherit the Craft Application enabling the power of Craft throughout the RESTful API.
  2. Versioned. A clean, semantic url: https://api.domain.com/v1/some... Independent. The API does not have to resolve using the same domain as the web site.
  3. Flexible. Supports CRUD operations with elements, models, components, etc. Endpoints can also execute actions that trigger emails, tasks and whatever custom logic you can think of.
  4. Authentication. Defined API actions can be placed behind authentication to prevent unwanted access. Authentication methods can consist of basic, token or session and are tied to a Craft User.

We had a handful of auxiliary criteria that we wanted to incorporate, but we’ll touch on those as we dig into the solutions for the requirements above.

Craft Inheritance

This proved to be one of the most delicate parts of the entire API solution. By default the Craft Application allows you to identify a ‘web’ and ‘console’ application type which loads the appropriate Request, UrlManager, Response and a handful of other specific classes. These application types follow the Yii2 framework and simply adds a little Craft sugar on top. Although our API would fall under the ‘web’ application type, we ran into a few issues of using Craft’s application bootstrap and ended up creating our own (much of which is copied). I think there is room to work with Craft to support another application type (or brainstorm ways of handling application type configurations).

While we were able to inherit most of Craft, we chose to overwrite a few key classes:

  • Request. We opted to remove a few methods targeted at web browsers and disable CSRF by default. There’s an argument that we may not need this trimmed down Request class so it’s on the table for review down the road.
  • UrlManager. We chose to ignore all Craft routes. You can’t access the admin panel, element routes and custom routes are ignored. We constructed a mechanism to lazy-load a Yii2 Module (based on the first ‘versioned’ url segment) where specific url rules are explicitly defined. Opting to only load a subset of routes keeps the API thin, reducing the risk of url conflicts, and increases performance.
  • UserIdentity. To enable token based identities, we extended the UserIdentity.
  • Response. Since our primary response format is JSON, we decided to encode the response using the pretty spec.

There are a few other miscellaneous changes we made along the way, but we won’t bore you with those details. It’s worth noting that although we are loading our own Yii2 application, it is possible to override these core classes with Crafts native web and console applications.

Versioning

We strongly stand behind versioning your APIs. It’s simple to do and gives you the ability to easily iterate on features without affecting your consumers. Given our position on versioning, the entire API solution requires it. As it turns out, using Yii2 modules and sub-modules make this extremely easy to implement.

  1. Extend our ‘ApiVersion’ module (where you also define your url rules).
  2. Define the newly created ‘versioned’ module in the API configuration like any normal Yii2 module.

To handle the construction of the endpoints, Yii2 enables us to simply define sub-modules with controllers. For example, a “Blog” sub-module with a “CategoryController” would serve a GET request for ‘/v1/blog/category/’. It’s a straightforward and modular approach. Keep in mind, you can nest multiple sub-modules to construct even more complex paths.

Domain Independent

For vanity reasons, an independent API domain allows us to serve the domain on an entirely different domain than the web site. For server architectural factors, this gives us the ability to host the API on a different infrastructure and isolate server maintenance windows between the web site and API. It’s worth noting that since we’re using a version based API url, you can safely use the same url as your web site.

Flexible

Although Craft has a powerful element foundation, we wanted the RESTful API to handle anything you could dream up. While the Yii2 native combination of sub-modules, controllers and actions allow you to construct pretty much anything, we made a few assumptions to assist along the way:

  • Serializing. We opted to incorporate the commonly used Fractal library to handle all of the data transformation and serialization. Yii2 ships with it’s own serializer, but our familiarity with Fractal teamed with how easy it is to use made us build our solution around it.

  • API Module Inheritance. To assist in the data serialization, we built an inheritance module. The module assists in much of the repetitive logic around serializing objects for output. Simply register an element type and the appropriate transformer and you’re well on your way.

  • Model / Element Inheritance. Many API endpoints interact with elements. A suite of common element services, controllers and actions can be leveraged to remove a level or redundancy and provide constancy throughout the API. We’ve got the verbs (GET, POST, PUT, PATCH, DELETE, OPTIONS, etc) covered and it’s easy to implement one or all of them.

Authentication

With the ability to RESTfully alter data, a level of authentication is needed. We have three primary means of authentication:

  • Basic: Username / Password
  • Token: A randomly generated string
  • Session: The current user's session

Access levels and authentication methods can be determined for each endpoint. Some endpoints can be left open, others may be locked down to admins only. API’s are typically stateless, and session based authentication is frowned upon, but with the API so closely tied to Craft, there are some unique circumstances that make session based authentication useful.

It’s also worth noting, we have a handful of enhancements we would like to perform around the authentication piece:

Conclusion

Working on the Craft 3 + RESTful API solution was a blast. There is still some uncertainty around whether Craft will implement a first party RESTful API solution, but until then we’ve got something that works very well. Our goal is to take it through another round of refactoring, cleaning up and documenting as we go in hopes of having a release around the time of Craft 3 beta.

In the meantime, here are a few working example from our blog built on Angular 2 and accesses our public API blog endpoint:

curl -X GET -H "Content-Type: application/json" "https://api.flipboxfactory.com/v1/blog"