5 March, 2020
Dear developers, have you ever worked on a new API? Those of you who have probably know that before writing the first endpoint, you need to determine the format in which the request parameters will be passed and the response returned. For inexperienced developers that might be a bother but I just might have the solution to this annoying problem. It’s JSON-API – a set of ready-made rules and recommendations, specifying all elements of the endpoint interface. So if you want to learn how to create an API in PHP with JSON-API, follow me!
Why do I need JSON-API?
Since there is no single right solution for the API parameter schema and responses, any approach that coherently and consistently formulates all input and output data is sufficient. Meeting these conditions will ensure easy integration of customer applications and reduce the number of errors during development.
Having said that, if you decide to use your own structure, you should think about a few things. In terms of requests, it will be filters, pagination and sorting. As for the responses – formatting returned objects fields, nesting of related objects, metadata (e.g. pagination) and error messages.
It’s common practice to start implementation without comprehensive interface planning and then adding further elements as the work progresses and new needs arise. However, this strategy can lead to losing consistency and creating a pattern too difficult to automatically process in the client application.
If you don’t have your own proven and refined approach to this issue, it is worth applying a ready-made solution. I suggest looking for ideas used by a wider group of programmers – this way you will surely avoid all the aforementioned problems, save time and focus on the implementation of business logic and other priority tasks. And that’s where JSON-API comes in.
What is JSON-API?
Citing the standard documentation, JSON-API is a specification of how to send requests to read or modify a resource, and how the server responds to those requests. The data transport format is JSON, and requests and responses are sent as:
Request to the server according to JSON-API
So let’s start by building requests, focusing on resource collection (GET requests). In this case, all relevant data is transmitted in the URL. The JSON-API standard specifies how to name query string parameters. The parameters are divided into five groups:
- Range of returned fields
- Including related objects
For the purposes of this article, we will use a hypothetical endpoint to download a list of products:
Pagination is the simplest of the above-mentioned parameters. It typically consists of two values, specifying the number of elements per one “page” and which “page” is to be returned. There are various strategies based on page number and size, or limit and offset. In terms of pagination, JSON-API only specifies that the parameter should be an array called
page, leaving you the freedom to choose the strategy. Example:
GET /products?page[limit]=20&page[offset]=60 # return products 61-80
GET /products?page[size]=20&page[number]=2 # return products 41-60
Sorting results in accordance with the JSON-API recommendations should be carried out using the
sort parameter. The parameter values should refer to the field names of returned objects (although this is not a strict requirement, you’re allowed to bend this rule a little). Example – sort by title:
It is possible to sort by more than one attribute. In this case, the subsequent attributes should be separated by a comma. On the server’s side, sorting should be carried out in order from the first attribute to the last – in the example below you should sort first by the date of publication and then by title.
The default sorting direction is ascending (ASC). In order to change the direction to descending (DESC), any of the attributes should be preceded by a minus sign, as presented below:
The specification also provides sorting by fields from related objects. In this case, you should provide a path consisting of attribute names that lead to the desired field, separated by periods. Example – sorting by product category name:
Filtering results is probably the most complex issue here. But fear not – JSON-API leaves a lot of freedom, requiring only that all filtering data be sent in an array parameter called (unsurprisingly) a
filter. For example, filters can be passed in the request as follows:
GET /products?filter[name]=SSD drive
Similarly to sorting, filtering can apply to the associated object attribute. In a situation like that, you can use the same syntax:
We can assume that the basic filters are strict filters, i.e. they require a perfect match of the filter value to the attribute. If you need to filter the range of values (e.g. minimum and/or maximum value for a numeric attribute) you can add suffix
__max (double underscore is necessary so that the server is able to distinguish the range filter from the field ending with the word min or max). Let’s look at an example:
Depending on your domain requirements, you can define additional suffixes and corresponding filtering methods, e.g.
__like etc. Remember to maintain consistency – a specific suffix (as well as its absence) must cause the same behaviour in every filter. For example, if it is assumed that filters without a suffix work on the exact match principle, there can be no case of a filter that works differently.
4. Range of returned fields
The range of returned fields can be used to reduce the amount of data in the server response. For example, when downloading a list of items, you may not always need to download the date when the item was added to the offer, or availability in various stores – you may only need the name and price. In such cases, you can explicitly provide in the request which fields are to be returned in the response. For this purpose, the JSON-API reserves a parameter called
fields. The syntax is as follows – fields should be listed after the decimal point. Fields from the object to which the request relates should be explicitly mentioned:
Fields from related objects should be listed with the path leading to the object. The following example specifies that a product with id = 1 should be returned, together with the name of the category and the name and shipment price available for the packaging in which the product is delivered.
5. Including related objects
I’ve already mentioned including related objects several times in this article (just checking if you were paying attention 😏). You need to explicitly indicate in the request which of the objects associated with the main object should be included in the response. A good example is returning products along with customer feedback. This allows you to reduce the number of queries to the server, which always positively influences the performance of the application. According to the JSON-API rules, the request parameter for this purpose is called
include. Its value is a list of paths to the desired associations, separated by a comma:
Just like in previous cases, reference may be made to further associations:
Server responses according to JSON-API
The content of the GET request response must be saved in JSON format. The structure of the response is strictly defined. The first level of response must contain at least one of the keys:
meta key can be used to return any additional information. An example of its use can be information about the pagination of results:
errors key should contain messages from potential errors. JSON-API defines this key as an array of objects. Each of them may have several of the defined keys. The most important of them are
data key is intended for storing the main content of the response, i.e. in most cases the content of objects returned by an endpoint. This response section is most closely defined in the JSON-API. The returned resource (single object or array of objects) must appear directly under the
data key. Each resource must be returned in the form of an object containing
The first key must contain a unique, global name for the object type, which is immutable within the entire API for each object (e.g. product). The second key is the identifier – it can successfully be the primary key from the database. Other allowed keys are
meta. From this set,
relationships are the most commonly used for transporting other object fields and information about related objects. Below you can find an example using the most important of the keys I’ve just described:
Please note that the
relationships section doesn’t show related object fields, only types and identifiers. This information is sufficient for the client application to automatically read further objects.
Related objects can also be returned in the same request. For the server to add these objects to the response, they must be listed in the
include parameter. As a result, the JSON document with the response will receive another key called
included, which will contain the full data of the indicated objects (or only selected fields if they are specified by the
fields parameter). Example:
As the example shows, the
included section contains data about the objects previously listed in the product
JSON-API in PHP applications
The tobyzerner/json-api-php project allows quick implementation of the JSON-API standard in PHP applications. You can quickly build responses in accordance with all the rules described above. Installation with the help of a composer:
composer require tobscure/json-api
To parse the parameters contained in the request, you can use the
Parameters class. It provides methods of access to the parameters (described in the first part) defined by the JSON-API standard:
Arrays with acceptable names should be provided for
sort parameters (parameters from URL will be automatically validated for correctness). You can specify the maximum
limit and default
offset for pagination.
To build a response in the controller of a given endpoint, all you have to do is create a collection object or a single resource, specifying in the constructor objects for serialization (e.g. Doctrine entities) and a serializer able to serialize a given object:
At this stage, you can indicate which of the related objects should be added to the response…
…and what fields of objects should be included in the response:
Then build a document and serialize it:
As you can see, the
Document object enriches the response with metadata:
Collection objects require you to point a serializer for each resource that you upload. At the same time, an abstract class is available, which can be used as a base to create your own serializers. In the minimum version, it is only necessary to implement a method that returns the attributes of a serialized object:
$type property is used in the
type key in the endpoint response. By default, the
id field uses the
$id of the serialized object, but you can add your own serializer method that will be used to retrieve the id:
For bundle mechanisms to be able to add related objects to responses, the serializer must have methods by which these objects can be retrieved and serialized. The names of these methods must be exactly the same as the names of the attached relations. For example, for product-related reviews and categories:
The mechanism above may work recursively, for example by adding the
user($review) method in the
ReviewSerializer class, you can attach reviews with user data to the product.
Need more information on JSON-API standard?
The information I provided in this article only addresses some of the most important issues regarding the JSON-API standard. So when it comes to basics, you are already a pro! If you are interested in creating API in PHP, documentation of JSON-API and practical examples are available on the project home page and Symfony bundle can be found on JSON-API Github. Good luck! 👍
Interested in developing microservices? 🤔 Make sure to check out our State of Microservices 2020 report – based on opinions of 650+ microservice experts!