Skip to content
intermediate Phase 60 · REST Implementation

Building Custom REST API

Creating custom REST API in Magento 2: API interface, webapi.xml route, ACL, and implementation

1h
0 problems
Topic Progress 0%

Module Structure

Custom API Module

app/code/Vendor/CustomApi/
├── registration.php
├── etc/
│   ├── module.xml
│   ├── webapi.xml
│   ├── acl.xml
│   └── di.xml
├── Api/
│   ├── CustomRepositoryInterface.php
│   └── Data/
│       └── CustomDataInterface.php
├── Model/
│   ├── CustomRepository.php
│   └── Data/
│       └── CustomData.php
└── Controller/
    └── Api/
        └── Custom.php

registration.php

<?php
use Magento\Framework\Component\ComponentRegistrar;

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

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="Vendor_CustomApi" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Webapi"/>
        </sequence>
    </module>
</config>

API Interface Definition

Data Interface

<?php
namespace Vendor\CustomApi\Api\Data;

interface CustomDataInterface
{
    const ID = 'id';
    const NAME = 'name';
    const EMAIL = 'email';
    const STATUS = 'status';
    const CREATED_AT = 'created_at';
    
    /**
     * @return int|null
     */
    public function getId(): ?int;
    
    /**
     * @param int $id
     * @return $this
     */
    public function setId(int $id);
    
    /**
     * @return string
     */
    public function getName(): string;
    
    /**
     * @param string $name
     * @return $this
     */
    public function setName(string $name);
    
    /**
     * @return string
     */
    public function getEmail(): string;
    
    /**
     * @param string $email
     * @return $this
     */
    public function setEmail(string $email);
}

Repository Interface

<?php
namespace Vendor\CustomApi\Api;

use Vendor\CustomApi\Api\Data\CustomDataInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;

interface CustomRepositoryInterface
{
    /**
     * Get by ID
     *
     * @param int $id
     * @return CustomDataInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getById(int $id): CustomDataInterface;
    
    /**
     * Get list
     *
     * @param SearchCriteriaInterface $searchCriteria
     * @return SearchResultsInterface
     */
    public function getList(
        SearchCriteriaInterface $searchCriteria
    ): SearchResultsInterface;
    
    /**
     * Save
     *
     * @param CustomDataInterface $data
     * @return CustomDataInterface
     */
    public function save(CustomDataInterface $data): CustomDataInterface;
    
    /**
     * Delete
     *
     * @param CustomDataInterface $data
     * @return bool
     */
    public function delete(CustomDataInterface $data): bool;
}

webapi.xml and ACL

webapi.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <router url="/V1">
        <!-- GET /V1/custom -->
        <route url="/custom" method="get">
            <service class="Vendor\CustomApi\Api\CustomRepositoryInterface" method="getList"/>
            <resources>
                <resource ref="anonymous"/>
            </resources>
        </route>
        
        <!-- GET /V1/custom/:id -->
        <route url="/custom/:id" method="get">
            <service class="Vendor\CustomApi\Api\CustomRepositoryInterface" method="getById"/>
            <resources>
                <resource ref="anonymous"/>
            </resources>
        </route>
        
        <!-- POST /V1/custom -->
        <route url="/custom" method="post">
            <service class="Vendor\CustomApi\Api\CustomRepositoryInterface" method="save"/>
            <resources>
                <resource ref="Vendor_CustomApi::custom_save"/>
            </resources>
        </route>
        
        <!-- PUT /V1/custom/:id -->
        <route url="/custom/:id" method="put">
            <service class="Vendor\CustomApi\Api\CustomRepositoryInterface" method="save"/>
            <resources>
                <resource ref="Vendor_CustomApi::custom_save"/>
            </resources>
        </route>
        
        <!-- DELETE /V1/custom/:id -->
        <route url="/custom/:id" method="delete">
            <service class="Vendor\CustomApi\Api\CustomRepositoryInterface" method="delete"/>
            <resources>
                <resource ref="Vendor_CustomApi::custom_delete"/>
            </resources>
        </route>
    </router>
</config>

acl.xml

<?xml version="1.0"?>
<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="Vendor_CustomApi::custom" title="Custom API">
                    <resource id="Vendor_CustomApi::custom_view" title="View Custom"/>
                    <resource id="Vendor_CustomApi::custom_save" title="Save Custom"/>
                    <resource id="Vendor_CustomApi::custom_delete" title="Delete Custom"/>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

Implementation

Data Object

<?php
namespace Vendor\CustomApi\Model\Data;

use Vendor\CustomApi\Api\Data\CustomDataInterface;
use Magento\Framework\Model\AbstractExtensibleModel;

class CustomData extends AbstractExtensibleModel implements CustomDataInterface
{
    public function getId(): ?int
    {
        return $this->getData(self::ID);
    }
    
    public function setId(int $id)
    {
        return $this->setData(self::ID, $id);
    }
    
    public function getName(): string
    {
        return $this->getData(self::NAME);
    }
    
    public function setName(string $name)
    {
        return $this->setData(self::NAME, $name);
    }
    
    public function getEmail(): string
    {
        return $this->getData(self::EMAIL);
    }
    
    public function setEmail(string $email)
    {
        return $this->setData(self::EMAIL, $email);
    }
}

Repository

<?php
namespace Vendor\CustomApi\Model;

use Vendor\CustomApi\Api\CustomRepositoryInterface;
use Vendor\CustomApi\Api\Data\CustomDataInterface;
use Vendor\CustomApi\Model\ResourceModel\Custom as CustomResource;
use Vendor\CustomApi\Model\CustomFactory;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;

class CustomRepository implements CustomRepositoryInterface
{
    public function __construct(
        private CustomResource $resource,
        private CustomFactory $customFactory,
        private SearchResultsInterfaceFactory $searchResultsFactory,
    ) {
    }
    
    public function getById(int $id): CustomDataInterface
    {
        $custom = $this->customFactory->create();
        $this->resource->load($custom, $id);
        
        if (!$custom->getId()) {
            throw new \Magento\Framework\Exception\NoSuchEntityException(
                __('Custom entity with ID %1 not found', $id)
            );
        }
        
        return $custom;
    }
    
    public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
    {
        $collection = $this->collectionFactory->create();
        
        // Apply search criteria
        $this->collectionProcessor->process($searchCriteria, $collection);
        
        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());
        
        return $searchResults;
    }
    
    public function save(CustomDataInterface $custom): CustomDataInterface
    {
        try {
            $this->resource->save($custom);
        } catch (\Exception $e) {
            throw new \Magento\Framework\Exception\CouldNotSaveException(
                __('Could not save custom entity: %1', $e->getMessage()),
                $e
            );
        }
        
        return $custom;
    }
    
    public function delete(CustomDataInterface $custom): bool
    {
        try {
            $this->resource->delete($custom);
        } catch (\Exception $e) {
            throw new \Magento\Framework\Exception\CouldNotDeleteException(
                __('Could not delete custom entity: %1', $e->getMessage()),
                $e
            );
        }
        
        return true;
    }
}

Quiz

1. Where is webapi.xml located?

Question 1 options

2. What element defines API permissions?

Question 2 options

3. What interface must data objects implement?

Question 3 options

Flashcards

Question

Where is webapi.xml?

Answer

app/code/Vendor/Module/etc/webapi.xml

Question

What maps routes to methods?

Answer

service class and method in webapi.xml

Question

How do you define API permissions?

Answer

Use resources with ACL ref

Question

What is the data object pattern?

Answer

Implement DataInterface with getters/setters

Question

How do you handle errors?

Answer

Throw specific exceptions like NoSuchEntityException

Revision Notes

Key Takeaways

  • 1. Define API interfaces in Api/ directory
  • 2. Configure routes in webapi.xml
  • 3. Set permissions via acl.xml
  • 4. Implement repository with error handling
  • 5. Use DataInterface for response structure

Interview Tips

  • Know the module structure for custom APIs
  • Understand webapi.xml configuration
  • Be ready to create a complete custom API
  • Discuss error handling patterns

Cheat Sheet

Module structure:
  etc/webapi.xml - Routes
  etc/acl.xml - Permissions
  Api/Interface.php - API contract
  Api/Data/Interface.php - Data structure
  Model/Repository.php - Implementation

webapi.xml:
  <route url="/custom" method="get">
      <service class="Vendor\Api\Interface" method="getList"/>
      <resources>
          <resource ref="anonymous"/>
      </resources>
  </route>