#RealMagento: Defining Custom REST API Endpoints in Magento 2

Welcome to another #RealMagento Magento 2 post! This post will cover extending the core REST API by defining our own custom API endpoint. The core API in Magento 2 already contains a lot of functionality and the extent of that functionality can be viewed in their excellent documentation by clicking here.

As with most custom functionality in Magento 2 we will be adding our REST API endpoint via a module. So let’s first create that module’s skeleton.

A quick side note; I am testing this code on Magento version 2.1.0. Keep that in mind if you are experiencing errors or seeing unexpected behavior. Theoretically though this code should at least work on all patch versions of 2.1.x. I will also assume that you are testing this module in /app/code/ and if you are not there may be other requirements pertaining to your /composer.json file for Magento to detect the module.

There is nothing out of the ordinary in the registration.php file which will be located at app/code/Demac/SetPasswordApi/registration.php and its contents are:

<?php

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Demac_SetPasswordApi', __DIR__);

There is also nothing out of the ordinary in the module.xml file which will be located at app/code/Demac/SetPasswordApi/etc/module.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
   <module name="Demac_SetPasswordApi" setup_version="0.1.0">
   </module>
</config>

This is the acl.xml file that will control access to our API endpoint. It is located at app/code/Demac/SetPasswordApi/etc/acl.xml and its contents are:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
   <acl>
       <resources>
           <resource id="Magento_Backend::admin">
               <resource id="Demac_SetPasswordApi::main" title="Demac_SetPasswordApi" sortOrder="999">
                   <resource id="Demac_SetPasswordApi::setPassword" title="Set Customer Password" sortOrder="10"/>
               </resource>
           </resource>
       </resources>
   </acl>
</config>

We will need to define our API endpoints in the webapi.xml file located at app/code/Demac/SetPasswordApi/etc/webapi.xml with the following contents:

<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
   <route url="/V1/demac_setPasswordApi/customers/setPassword" method="POST">
       <service class="Demac\SetPasswordApi\Api\CustomerRepositoryInterface" method="setPassword"/>
       <resources>
           <resource ref="Demac_SetPasswordApi::setPassword"/>
       </resources>
   </route>
</routes>

The first thing that I will mention in the above XML definitions is that I am using the “Demac_SetPasswordApi::setPassword” ACL resource. This will require us to provide credentials that have permission to access that resource in the ACL when we make our API request to retrieve an access token. Alternatively, if you don’t require clients to authenticate to access your endpoint or you want to simplify things for testing purposes you could instead use the “anonymous” resource. I will show an example of both near the end of the post.

The and XML nodes are self-explanatory. They define the RESTful resource path and HTTP request verb, and the class and method pair that are responsible for handling the request respectively.

Define your Interfaces

Next we will define our interfaces. We are using interfaces as opposed to directly depending on models in our /etc/webapi.xml file as this decouples the public contract that our API endpoint exposes from its implementation. This way, as long as whichever model we use complies with our single interface we can switch it out for another model whenever we want without breaking our API clients’ functionality.

This is the interface that will represent our API endpoint and it will be located at app/code/Demac/SetPasswordApi/Api/CustomerRepositoryInterface.php with the following contents:

<?php

namespace Demac\SetPasswordApi\Api;


interface CustomerRepositoryInterface
{
   /**
    * @param int|string $id
    * @param string $password
    * @return \Demac\SetPasswordApi\Api\Data\CustomerInterface
    */
   public function setPassword($id, $password);
}

Next we will specify the interface for our data model. This model will simply act as our custom data structure for passing related data around together. The interface will be located at app/code/Demac/SetPasswordApi/Api/Data/CustomerInterface.php and its contents are:

<?php

namespace Demac\SetPasswordApi\Api\Data;


interface CustomerInterface extends \Magento\Framework\Api\CustomAttributesDataInterface
{
   /**
    * @return string|null
    */
   public function getPassword();

   /**
    * @param string|int $newPassword
    * @return null
    */
   public function setPassword($newPassword);

   /**
    * @return int|null
    */
   public function getId();

   /**
    * @param int|null $newId
    * @return null
    */
   public function setId($newId);
}

One very important note regarding the two above interfaces is that the PHP DocBlocks must be accurate and any class names must be fully-qualified. If either of these requirements aren’t met you will almost certainly experience difficult to track down errors when attempting to test your API endpoint.

Define your models

Next we will define the models that will implement the above interfaces. We will start with the CustomerRepository model located at app/code/Demac/SetPasswordApi/Model/CustomerRepository.php with the following contents:

<?php

namespace Demac\SetPasswordApi\Model;


use Demac\SetPasswordApi\Api\CustomerRepositoryInterface;
use Demac\SetPasswordApi\Api\Data\CustomerInterfaceFactory;
use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\CustomerRepositoryInterface as CoreCustomerRepository;
use Magento\Customer\Model\CustomerRegistry;
use Magento\Framework\Exception\State\InvalidTransitionException;

class CustomerRepository implements CustomerRepositoryInterface
{
   /**
    * @var CustomerInterfaceFactory
    */
   private $customerFactory;

   /**
    * @var CoreCustomerRepository
    */
   private $customerRepository;

   /**
    * @var AccountManagementInterface
    */
   private $accountManager;

   /**
    * @var CustomerRegistry
    */
   private $customerRegistry;

   /**
    * CustomerRepository constructor.
    * @param CustomerInterfaceFactory $customerFactory
    * @param CoreCustomerRepository $customerRepository
    * @param AccountManagementInterface $accountManager
    * @param CustomerRegistry $customerRegistry
    */
   public function __construct(
       CustomerInterfaceFactory $customerFactory,
       CoreCustomerRepository $customerRepository,
       AccountManagementInterface $accountManager,
       CustomerRegistry $customerRegistry)
   {
       $this->customerFactory = $customerFactory;
       $this->customerRepository = $customerRepository;
       $this->accountManager = $accountManager;
       $this->customerRegistry = $customerRegistry;
   }

   /**
    * @param int|string $id
    * @param string $password
    * @return \Demac\SetPasswordApi\Api\Data\CustomerInterface
    */
   public function setPassword($id, $password)
   {
       try {
           $this->validateRequestData($id, $password);
           /** @var \Magento\Customer\Model\Customer $customer */
           $customer = $this->customerRepository->getById($id);
           $passwordHash = $this->accountManager->getPasswordHash($password);
           $customerSecure = $this->customerRegistry->retrieve($customer->getId());
           $customerSecure->setPasswordHash($passwordHash);
           $this->customerRepository->save($customer);
           $dataModel = $this->customerFactory->create();
           $dataModel->setId($customer->getId());
           $dataModel->setPassword($password);
           return $dataModel;
       } catch(\Exception $e) {
           throw new InvalidTransitionException(__('An error occurred and we could not set the password'));
       }
   }

   /**
    * @param $id
    * @param $password
    */
   private function validateRequestData($id, $password)
   {
       if (!isset($id) || !isset($password) || $password === '') {
           throw new \InvalidArgumentException('Both id and password must be valid values.');
       }
   }
}

The bulk of the logic in the above model is in the setPassword() method which just uses the input id to retrieve the customer it then hashes the input password and sets that hash on the customer instance and saves it. This method will return the input data through an instance of our data model if the request is successful. All of the dependencies of the model are injected through the constructor and their use in the class is self-explanatory.

Define Your Data Model

Next we will define the data model. It is located at app/code/Demac/SetPasswordApi/Model/Data/Customer.php with the following contents:

<?php

namespace Demac\SetPasswordApi\Model\Data;


use Demac\SetPasswordApi\Api\Data\CustomerInterface;

class Customer extends \Magento\Framework\Api\AbstractExtensibleObject implements CustomerInterface
{

   /**
    * @return string|null
    */
   public function getPassword()
   {
       return $this->_get('password');
   }

   /**
    * @param string|int $newPassword
    * @return null
    */
   public function setPassword($newPassword)
   {
       $this->setData('password', $newPassword);
   }

   /**
    * @return int|null
    */
   public function getId()
   {
       return $this->_get('id');
   }

   /**
    * @param int|null $newId
    * @return null
    */
   public function setId($newId)
   {
       $this->setData('id', $newId);
   }
}

Final Step: Set Preferences for the Interfaces Being Defined

The final step before we test our custom API endpoint is to create a di.xml file that will set the preferences for the interfaces that we have defined. This is necessary as otherwise Magento would not know which implementation of our interface to use at runtime and so would not be able to resolve dependencies on those interfaces.

The file is located at app/code/Demac/SetPasswordApi/etc/di.xml with the following contents:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
   <preference for="Demac\SetPasswordApi\Api\Data\CustomerInterface" type="Demac\SetPasswordApi\Model\Data\Customer"/>
   <preference for="Demac\SetPasswordApi\Api\CustomerRepositoryInterface" type="Demac\SetPasswordApi\Model\CustomerRepository"/>
</config>

Ok so now it is time to test our custom REST API endpoint. Before I show you the commands to do so ensure that your module is enabled and registered by running the following commands:

$ bin/magento module:enable Demac_SetPasswordApi
$ bin/magento setup:upgrade

We will also need to <strong>clear cache(including the di cache)</strong> using the following commands:


$ bin/magento cache:flush
$ rm -rf var/generation

You can confirm that the module is enabled by looking through the output of this command:

$ bin/magento module:status

Test Your Custom API Endpoints

Now that we have ensured our module is enabled I will show you how I would recommend testing the custom API endpoints. Here I will use the curl command-line tool to test but you can use whatever you find easiest as your API client. We first need to authenticate with Magento to receive an access token that we will need to send with all subsequent requests(unless the endpoints specify the “anonymous” resource, then skip this first step).

$ curl -X POST “http://example.com/rest/V1/integration/admin/token” \
-H “Content-Type: application/json”
-d ‘{“username”: “<your username>”, “password”: “<your password>”}’

I also want to note that the admin portion of the RESTful resource path in the above URL would require your backend account to have a user role of administrators.

Next let’s make a request to our custom API endpoint to update the password of the user whose id is 1. You can change this id value accordingly based on what customers exist on your Magento installation. The endpoint requires the two data keys id and password where id is an integer value and password must be a string. We know this by looking at the function signature of the setPassword() method of the CustomerRepositoryInterface class and the method’s PHP DocBlock, shown again here:

/**
* @param int|string $id
* @param string $password
* @return \Demac\SetPasswordApi\Api\Data\CustomerInterface
*/
public function setPassword($id, $password);


$ curl -X GET “http://example.com/rest/V1/demac_setPasswordApi/customers/setPassword” \
-H “Authorization: Bearer <our access token>” \
-H “Content-Type: application/json” \
-d ‘{“id”: 1, “password”: “mynewpassword”}’

I mentioned earlier that if you use the “anonymous” resource for your endpoint that you don’t need to provide an access token and thus you don’t need to authenticate in the first place. In that case the following command would work to test your custom API endpoint, although providing an access token would NOT cause any problem either:

$ curl -X GET “http://example.com/rest/V1/demac_setPasswordApi/customers/setPassword” \
-H “Content-Type: application/json” \
-d ‘{“id”: 1, “password”: “mynewpassword”}’

For a successful request using the above request data you should see this response:

{"password":"mynewpass","id":13}

Write Tests for the Above Module!

ecommerce, website design, demac media, magento 2, custom rest api endpoint, magento ecommerce,magento 2 rest api endpoint

As a final suggestion I would recommend that you look into writing some tests for the above module. I think that custom API endpoints are a great use case for integration tests and you can learn more about those tests and getting started with testing in Magento 2 in general in one of our previous posts, view it by clicking here.