Building a RESTful API in Craft CMS

June 1, 2022
by Ben Croker

There are times when you want to make data and actions in Craft available to external services. Fortunately, Craft’s URL manager (or more accurately Yii’s) makes handling the routing of requests straightforward, so you can define and route your API endpoints with just a few lines of code.

Restful api

There is often confusion around what a REST API actually is. For a deep dive on that I’ll refer you to restfulapi​.net. What’s important to note is that an API is considered RESTful if it follows the 6 guiding principles or architectural constraints:

  1. Uniform interface
  2. Client-server
  3. Stateless
  4. Cacheable
  5. Layered system
  6. Code on demand (optional)

URL Rules #

For the sake of simplicity, we’re going to assume that we want to create an API with the following functionality:

  1. Return whether a user with a specific ID exists.
  2. Update a custom field value with handle subscriptionStatus on an existing user by ID.
  3. Delete a user by ID.

For each of these functions, we’ll define a HTTP method (verb) and endpoint URL.

  1. GET api/users/<userId>
  2. PUT api/users/<userId>
  3. DELETE api/users/<userId>

Note how in this case our URL endpoints are all the same. We use HTTP methods to describe API functionality, rather than unique URLs which if used would violate the Uniform Interface guideline.

To define the API routing, we need to register the URL rules above, and we can do this in several places such as when defining components in our config/app.php file, in our config/routes.php file, in a custom plugin/​module or at runtime.

Here’s how we can define URL rules and map them to controller actions in the config/routes.php file.

return [
    'GET api/users/<userId:\d+>' => 'rest-api/users/exists',
    'PUT api/users/<userId:\d+>' => 'rest-api/users/update',
    'DELETE api/users/<userId:\d+>' => 'rest-api/users/delete',
];

The array keys define the routes and the values define the controller actions that should be called. We define the route as the HTTP method (optional) followed by a URL pattern that ends with the token <userId:\d+>, which represents one or more digits d+ that is stored in a named query parameter userId.

URL rules

Using a Custom Plugin #

Since we’ll anyway have to create a custom plugin (or module) to handle the controller actions, let’s look at an example of defining our URL rules directly in the plugin’s main class. We do this by merging our rules onto the rules in the EVENT_REGISTER_SITE_URL_RULES event. This will help to establish the rules as part of the plugin functionality, rather than that of the site, which we may or may not want depending on our use-case. 

use craft\base\Plugin;
use craft\events\RegisterUrlRulesEvent;
use craft\web\UrlManager;
use yii\base\Event;

class RestApi extends Plugin
{
    public function init()
    {
        parent::init();

        Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_SITE_URL_RULES,
            function(RegisterUrlRulesEvent $event) {
                $event->rules = array_merge($event->rules, [
                    'GET api/users/<userId:\d+>' => 'rest-api/users/exists',
                    'PUT api/users/<userId:\d+>' => 'rest-api/users/update',
                    'DELETE api/users/<userId:\d+>' => 'rest-api/users/delete',
               ]);
            }
        );
    }
}

Next, we’ll define each of the action methods in our controller.

use craft\web\Controller;

class UsersController extends Controller
{
    public function actionExists(int $userId): Response
    { 
    }

    public function actionUpdate(int $userId): Response
    { 
    }

    public function actionDelete(int $userId): Response
    { 
    }
}

Note how the user ID part of the URL automatically becomes available as an action parameter named userId, which we’ll later use inside our action methods.

API Considerations #

There are a few things that we’ll likely want to add to our controller to secure the API endpoint and make it work as expected. Firstly, we’ll disable CSRF validation, since this is an API endpoint and CSRF tokens should not be required. Secondly, we’ll allow anonymous access to the controller, so that it can be accessed externally without requiring a login. Thirdly, since we’re allow the reading and writing of user data, we’ll add basic authentication in the form of an API key that we’ll store as an environment variable REST_API_KEY.

use craft\helpers\App;
use craft\web\Controller;
use yii\web\HttpException;

class UsersController extends Controller
{
    public bool $enableCsrfValidation = false;
    
    protected bool $allowAnonymous = true;

    public function beforeAction($action): bool
    {
        if (!parent::beforeAction($action)) {
            return false;
        }

        $key = Craft::$app->getRequest()->getParam('key', '');
        $apiKey = App::parseEnv('REST_API_KEY');

        // Verify provided key against API key
        if (empty($key) || empty($apiKey) || $key != $apiKey) {
            throw new HttpException(403, 'Unauthorised API key');
        }

        return true;
    }
    
    public function actionExists(int $userId): Response
    { 
    }

    public function actionUpdate(int $userId): Response
    { 
    }

    public function actionDelete(int $userId): Response
    { 
    }
}

By verifying the key provided in the request against the value of the environment variable REST_API_KEY in the beforeAction method, we ensure that actions can only be called if this test passes. We could, of course, use more sophisticated authentication protocols such as OAuth 2.

Finally, for the sake of completeness, we’ll fill in our action methods, each of which returns a JSON encoded response with a value of true or false to indicate whether the response to the request was affirmative (successful).

use Craft;
use craft\elements\User;
use craft\helpers\App;
use craft\web\Controller;
use yii\web\HttpException;
use yii\web\Response;

class UsersController extends Controller
{
    public $enableCsrfValidation = false;
    
    protected int|bool|array $allowAnonymous = true;

    public function beforeAction($action): bool
    {
        if (!parent::beforeAction($action)) {
            return false;
        }

        $key = Craft::$app->getRequest()->getParam('key', '');
        $apiKey = App::parseEnv('$REST_API_KEY');

        // Verify provided key against API key
        if (empty($key) || empty($apiKey) || $key != $apiKey) {
            throw new HttpException(403, 'Unauthorised API key');
        }

        return true;
    }

    public function actionExists(int $userId): Response
    {
        return $this->asJson(Craft::$app->users->getUserById($userId) !== null);
    }

    public function actionUpdate(int $userId): Response
    {
        $subscriptionStatus = $this->request->getParam('subscriptionStatus', 0);
        $user = Craft::$app->users->getUserById($userId);

        if ($user === null) {
            throw new HttpException(404, 'User not found');
        }
         
        $user->setFieldValue('subscriptionStatus', $subscriptionStatus);
        
        return $this->asJson(Craft::$app->elements->saveElement($user));
    }

    public function actionDelete(int $userId): Response
    {
        return $this->asJson(Craft::$app->elements->deleteElementById($userId, User::class));
    }
}

Testing API Endpoints #

We can test our API endpoints using cURL, updating the base URL, the user ID and the key accordingly.

curl -X GET http://localhost/api/users/555\?key\=1234567890

curl -X PUT http://localhost/api/users/555\?key\=1234567890 -d "subscriptionStatus=1"

curl -X DELETE http://localhost/api/users/555\?key\=1234567890

Yii RESTful Web Services #

Yii, the PHP framework on which Craft is built, provides a RESTful Web Service as well as a UrlRule class that sets up URL rules for your API with minimal effort.

By declaring the following rule in the application configuration:

[
    'class' => 'yii\rest\UrlRule',
    'controller' => 'user',
]

The following RESTful API endpoints will automatically route to the respective controller actions.

  • 'PUT,PATCH users/<id>' => 'user/update' (update a user)
  • 'DELETE users/<id>' => 'user/delete' (delete a user)
  • 'GET,HEAD users/<id>' => 'user/view' (return the details of a user)
  • 'POST users' => 'user/create' (create a new user)
  • 'GET,HEAD users' => 'user/index' (return a list of users)
  • 'users/<id>' => 'user/options' (process all unhandled verbs of a user)
  • 'users' => 'user/options' (process all unhandled verbs of user collection)

In addition to routing, there are helpers for managing resources, controllers, authentication, rate limiting, and more. If you’re comfortable with Yii and its concepts then this can be a big time saver, however if you’re more comfortable in Craft Land” or want more fine-grained control, then setting things up yourself, as we’ve seen above, is absolutely within reach.