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?
2. What element defines API permissions?
3. What interface must data objects implement?
Flashcards
Question
Where is webapi.xml?
Click to reveal answer
Answer
app/code/Vendor/Module/etc/webapi.xml
Question
What maps routes to methods?
Click to reveal answer
Answer
service class and method in webapi.xml
Question
How do you define API permissions?
Click to reveal answer
Answer
Use resources with ACL ref
Question
What is the data object pattern?
Click to reveal answer
Answer
Implement DataInterface with getters/setters
Question
How do you handle errors?
Click to reveal answer
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>