Insights

Flexible APIs

Magento 2 offers a robust workflow for creating API’s for your modules, from handling simple post requests, to more advanced search queries of your models. However, Magento 2 does present some constraints initially when determining what type of data your API calls should return.

Creating a generic API response for Magento 2

  • Magento Version: 2.2.3
  • Assumed Knowledge Level: Intermediate

This article was inspired by a particular problem I had in a client project recently. I was tasked with writing an API that required all the returned data to follow a strict structure. So I held my breath, and took a dive into the Magento 2 dev docs. What I found was that Magento’s API request protocols allow for the return of all scalar types such as ints, string, arrays, but it also supports returning objects with the use of interfaces . At first glance then, this task would can seem like a trivial matter, and you’re probably thinking “So what?”  For example if I were writing an API that returns customers I would declare a return type of say an array of customer interfaces and call it a day.

But what if we’re dealing with multiple model types?

It can be cumbersome to have to declare specific return types for each of your API calls if you are dealing with multiple different models, or worse, you’re not sure what models are going to be needed to be returned from your API as the project grows. For example, if we were building a blog inside our Magento store, here we could be dealing with posts, customers, comments, likes, notifications etc.; as well as potentially any of Magento’s own model types. Magento’s API protocol is very strict when declaring what type of data your API calls should return, and when we’re dealing with objects Magento expects an interface of that model as the declared return type. That means we would we need to write interfaces for each of the models returned in all of all of our API calls: no thanks! What if we wanted a generic return type that could handle any model we throw at it? This would also provide the added bonus of being consistently structured, so that anyone who wants to use it has a consistent experience. Read on and you’ll see my solution to this problem.

Generic Response Object

Note the @api tag in the block comments above the interface declaration. This tells Magento2 to use this when describing our API in the WYSIWIG (if you’re using SOAP).

Firstly let’s create our generic response object. This is going to be what we tell any of our API calls to return which will comply with Magento’s API protocol. We start with writing its interface; this is what we declare as the return type for any of our API requests. Magento 2 will read this when running through its API protocol and check that all return types in this interface are allowed.

<!--?php namespace C3\CRUD\Api\Data; /** * @api */ interface DataResponseInterface { const MESSAGE = "message"; const RESPONSE_DATA = "data"; /** * @return string */ public function getMessage(); /** * @param string $message * @return $this */ public function setMessage($message); /** * @return \C3\CRUD\Api\Data\AbstractModelInterface[]|null */ public function getResponseData(); /** * * @param \C3\CRUD\Api\Data\AbstractModelInterface[] $data * @return $this */ public function setResponseData($data); } &lt;/pre&gt; &lt;p&gt;&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;&lt;br ?-->
Next, we’ll declare our concrete class that implements this interface.
<!--?php namespace C3\CRUD\Model\Data; use C3\CRUD\Api\Data\DataResponseInterface; use Magento\Framework\Api\AbstractSimpleObject; class DataResponse extends AbstractSimpleObject implements DataResponseInterface { /** * @return string */ public function getMessage() { return $this-&gt;_get(self::MESSAGE); } /** * @param string $message * @return $this */ public function setMessage($message) { $this-&gt;setData(self::MESSAGE, $message); return $this; } /** * @return \C3\CRUD\Api\Data\AbstractModelInterface[]|null */ public function getResponseData() { return $this-&gt;_data[self::RESPONSE_DATA] ?? null; } /** * * @param \C3\CRUD\Api\Data\AbstractModelInterface[] $data * @return $this */ public function setResponseData($data) { foreach($data as $datum) { $this-&gt;_data[self::RESPONSE_DATA][] = $datum; } return $this; } } &lt;/pre&gt; &lt;p&gt;&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;You may have noticed for the response data we’re returning an array of Abstract Model Interfaces. This is what gives us our flexibility to return anything in the response data and still make Magento happy that we’re returning a defined return type.&lt;/span&gt;&lt;/p&gt; &lt;h3&gt;&lt;span style="font-weight: 400;"&gt;Abstract Model &lt;/span&gt;&lt;/h3&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;Magento extends almost all of its models from the Abstract model class. This is the perfect candidate as a return type for our generic API response, but here we run into a problem. Magento 2 only allows interfaces and scalar types to be returned by an API. How do we tell Magento that our API calls are going to return an Abstract Model class? &lt;/span&gt;&lt;/p&gt; &lt;blockquote&gt; &lt;h4&gt;&lt;strong&gt;&lt;em&gt;Dependency trickery to the rescue!&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt; &lt;/blockquote&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;The simple solution is to first create an empty interface.&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;&lt;/p&gt; &lt;pre lang="PHP"&gt; &lt;?php namespace C3\CRUD\Api\Data; /** * @api */ interface AbstractModelInterface { } &lt;/pre&gt; &lt;p&gt;&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;Next we’re going to tell Magento 2 to preference the already existing Abstract Model whenever we reference this interface in our generic response. While we’re at it we’re going to tell Magento to prefer our concrete DataResponse object for our date response interface as well. &lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style="font-weight: 400;"&gt;&lt;/p&gt; &lt;pre lang="xml"&gt; &lt;?xml version="1.0"?-->

Usage

Now we have a generic return type of any API call we write with the benefit of having a consistent response for anyone who wants to use our modules API. For example from my previous blog post I created a simple CRUD module that created notes. Building on that example, let’s register an API endpoint in our webapi.xml file for Magento to reference.

<!--?xml version="1.0"?-->

Now we write out our Note Repository class that our new end point references.

<!--?php namespace C3\CRUD\Model; use C3\CRUD\Model\Data\DataResponse; use C3\CRUD\Model\ResourceModel\Note\Collection; use C3\CRUD\Model\ResourceModel\Note\CollectionFactory; class NoteRepository { /** * @var CollectionFactory */ private $noteCollection; public function __construct( CollectionFactory $noteCollection ) { $this-&gt;noteCollection = $noteCollection; } /** * @param $id * @return DataResponseInterface */ public function getNoteById($id) { /** @var Collection $noteCollection */ $noteCollection = $this-&gt;noteCollection-&gt;create(); $note = $noteCollection-&gt;addFieldToFilter('note_id', $id)-&gt;getFirstItem(); $response = new DataResponse(); $response-&gt;setMessage("Note retrieved!") -&gt;setResponseData($note); return $response; } } &lt;/pre&gt; &lt;p&gt;&lt;/span&gt;&lt;br ?-->
<span style="font-weight: 400;">
Now if we run a quick GET request to our new API endpoint to get a note with the ID of 1:</span>

https://YOUR SITE DOMAIN/rest/V1/crud/notes/getById/1

Then our user will be greeted with the following response in some prettily formatted JSON:

{
    "message": "Note retrieved!",
    "response_data": [
         {
            "note_id": 1,
            "body": "Lorem Ipsum",
            "created_at": "2018-06-22 14:07:20",
            "updated_at": "2018-06-22 14:07:20"
        }
    ]
}

This approach allows us now to return any object we like in the response data key. It provides all the flexibility we need to write our API return types as well as a consistent response object for any poor soul that wants to use our API. A consistent response structure is particularly appreciated when dealing with mobile application or frontend development, where the developer on the other side just wants the data in a structured object, and lets them write a single handler for any response our API can throw at them. This in turn helps speed up the development process, as well as making life for the developer on the other end more bearable.

If you have any constructive feedback please leave a comment below. Our aim here is to provide solutions we’ve come up with to problems we’ve faced, but if you have a better solution don’t be shy to let us know!

About the author

Team C3