Aug 9 2017

How to make your CakePHP 3 API produce JSON API

In this follow-up post to How to add JWT Authentication to a CakePHP 3 REST API we will update our existing API so it will produce JSON API compliant output giving you the benefits of standardization and instant compatibility with JSON API supporting tools like Ember Data.

> API methods used in this post shared with Postman

Before We Begin

This is part five of the CakePHP 3 REST API tutorial series:

  1. How to build a CakePHP 3 REST API in minutes
  2. How to use a CakePHP 3 REST API
  3. How to prefix route a CakePHP 3 REST API
  4. How to add JWT Authentication to a CakePHP 3 REST API
  5. How to make your CakePHP 3 API produce JSON API
  6. How to use a CakePHP API as the data backend for Ember in 30 minutes

Before starting this tutorial either:

  • complete the previous tutorial
  • start fresh by using these end-state application sources, composer installing and running the database migration

1. Install required packages

To make sure your API is using an up-to-date version of CakePHP and the required version of the Crud plugin now update your project’s composer packages by running:

1
composer update

Update Crud to at least version 5.0.0:

1
composer require friendsofcake/crud:^5.0

Remove any previous version of the neomerx package:

1
composer remove neomerx/json-api

Lastly, add the Crud JsonApi listener to your application by running:

1
composer require friendsofcake/crud-json-api

2. Disable JWT authentication

Since JWT authentication is unrelated to JSON API we will disable it by removing the related lines in src/Controller/Api/AppController which should leave you with a file looking like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace App\Controller\Api;
use Cake\Controller\Controller;
use Cake\Event\Event;
/**
* AppController specific to API resources
*/
class AppController extends Controller
{
use \Crud\Controller\ControllerTrait;
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Crud.Crud', [
'actions' => [
'Crud.Index',
'Crud.View',
'Crud.Add',
'Crud.Edit',
'Crud.Delete'
],
'listeners' => [
'Crud.Api',
'Crud.ApiPagination',
'Crud.ApiQueryLog'
]
]);
}
}

Even though the CakePHP and Crud upgrades were totally transparent while writing this post this might be a good moment to verify that your API’s cocktails endpoint is still producing the expected results using GET, POST, etc.

3. Enable JSON API

All that is needed now to make your API produce JSON API is opening src/Controller/Api/AppController and replacing the Crud.Api listener with Crud.JsonAPi so it looks like:

1
2
3
4
5
'listeners' => [
'CrudJsonApi.JsonApi',
'CrudJsonApi.Pagination', // Pagination != ApiPagination
'Crud.ApiQueryLog'
]

All done, seriously.

4. Using the new API

Please note that the JsonApiListener documentation contains very detailed usage descriptions and since there no point in duplicating them here we will provide you with just two basic examples to get you going.

The Postman collection contains examples of index, view, add, edit and delete.

Never forget:

  • ALL requests to your new API MUST use the application/vnd.api+json Accept Header
  • ALL requests with post data MUST use the application/vnd.api+json Content-Type Header

View action (GET)

For this example we are in the mood for a Mojito so we query http://cake3api.app/cocktails/3 making sure we are using:

  • HTTP Method GET
  • Accept Header application/vnd.api+json
API Request Headers for view action

If things went well your API should return Status Code 200 (Success) with a response body in JSON API format looking similar to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": {
"type": "cocktails",
"id": "3",
"attributes": {
"name": "Mojito",
"description": "Rum based",
"created": "2015-04-11T09:52:01+00:00",
"modified": null
},
"links": {
"self": "/api/cocktails/3"
}
}
}

Add action (POST)

To create a new cocktail send a JSON API request to http://cake3api.app/cocktails making sure you are using:

  • HTTP Method GET
  • Accept Header application/vnd.api+json
  • Content-Type Header application/vnd.api+json
API Request Headers for add action

Also make sure to set the full or partial body data in (absolutely) correct JSON API format, e.g:

1
2
3
4
5
6
7
8
9
{
"data": {
"type": "cocktails",
"attributes": {
"name": "Some cocktail",
"description": "Some description"
}
}
}

Now send the request.

If things went well your API should return Status Code 201 (Success) with a response body in JSON API format looking similar to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": {
"type": "cocktails",
"id": "24",
"attributes": {
"name": "Some name",
"description": "None inspired description",
"created": "2017-03-16T19:01:57+00:00",
"modified": "2017-03-16T19:01:57+00:00"
},
"links": {
"self": "/api/cocktails/24"
}
}
}

5. Enabling CORS middleware

Your API will be pretty useless if people (from other domains) won’t be able to use it so let’s enable it right now along with CakePHP’s new middleware functionality by following the book’s step-by-step instructions :

  1. update webroot/index.php
  2. create src/Application.php by copying from the cakephp-app repo

Now that our application is capable of handling middleware let’s add the cakephp-cors middleware plugin by running:

1
composer require ozee31/cakephp-cors

Enable the plugin by adding this to config/bootstrap.php:

1
Plugin::load('Cors', ['bootstrap' => true, 'routes' => false]);

To complete, specify the correct ExceptionRenderers in config/app.php so the JSON API (validation) errors keep functioning as expected whilst also respecting CORS headers:

1
2
3
4
5
6
7
8
9
10
11
'Error' => [
'errorLevel' => E_ALL & ~E_DEPRECATED,
'exceptionRenderer' => '\Crud\Error\JsonApiExceptionRenderer',
'skipLog' => [],
'log' => true,
'trace' => true,
],
'Cors' => [
'exceptionRenderer' => '\Cors\Error\AppExceptionRenderer'
],

Please note that the plugin will allow CORS for all origins, all methods and all headers by default which is a very good thing as we will start using CORS pretty heavily in the next tutorial.

6. Make it better

Even though the JsonApiListener is already quite feature-complete, some parts of the JSON API specification (like Sparse Fieldsets and query parameters) have not been implemented yet. Feel free to submit a PR for missing functionality and help work towards a full-featured implementation of the specification, the effort should be minimal.

Additional reading

Hat tip to josbeir for the inspiration found in his plugin.