Module Structure and Service Contracts
Module Structure
app/code/Vendor/WarrantyApi/
├── registration.php
├── etc/
│ ├── module.xml
│ ├── webapi.xml
│ ├── acl.xml
│ └── di.xml
├── Api/
│ ├── WarrantyRepositoryInterface.php
│ ├── WarrantyManagementInterface.php
│ └── Data/
│ ├── WarrantyInterface.php
│ └── WarrantySearchResultsInterface.php
├── Model/
│ ├── WarrantyRepository.php
│ ├── WarrantyManagement.php
│ ├── Warranty.php
│ ├── Data/
│ │ ├── Warranty.php
│ │ └── WarrantySearchResults.php
│ └── ResourceModel/
│ ├── Warranty.php
│ └── Collection.php
├── Exception/
│ ├── CouldNotSaveException.php
│ ├── NoSuchEntityException.php
c InvalidInputException.php
└── Api/
└── WarrantyInterface.php
Data Interface
<?php
namespace Vendor\WarrantyApi\Api\Data;
interface WarrantyInterface
{
const ID = 'id';
const ORDER_INCREMENT_ID = 'order_increment_id';
const PRODUCT_SKU = 'product_sku';
const CUSTOMER_EMAIL = 'customer_email';
const STATUS = 'status';
const EXPIRY_DATE = 'expiry_date';
const CREATED_AT = 'created_at';
const STATUS_ACTIVE = 'active';
const STATUS_EXPIRED = 'expired';
const STATUS_CLAIMED = 'claimed';
public function getId(): ?int;
public function setId(int $id);
public function getOrderIncrementId(): string;
public function setOrderIncrementId(string $orderIncrementId);
public function getProductSku(): string;
public function setProductSku(string $productSku);
public function getCustomerEmail(): string;
public function setCustomerEmail(string $customerEmail);
public function getStatus(): string;
public function setStatus(string $status);
public function getExpiryDate(): string;
public function setExpiryDate(string $expiryDate);
public function getCreatedAt(): ?string;
}
Repository Interface
<?php
namespace Vendor\WarrantyApi\Api;
use Vendor\WarrantyApi\Api\Data\WarrantyInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
interface WarrantyRepositoryInterface
{
public function getById(int $id): WarrantyInterface;
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;
public function save(WarrantyInterface $warranty): WarrantyInterface;
public function delete(WarrantyInterface $warranty): bool;
public function deleteById(int $id): bool;
}
Management Interface
<?php
namespace Vendor\WarrantyApi\Api;
use Vendor\WarrantyApi\Api\Data\WarrantyInterface;
interface WarrantyManagementInterface
{
public function getByOrderIncrementId(string $incrementId): WarrantyInterface;
public function claimWarranty(int $warrantyId): WarrantyInterface;
public function extendWarranty(int $warrantyId, string $newExpiryDate): WarrantyInterface;
}
webapi.xml, ACL, and Authentication
webapi.xml Routes
<?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">
<!-- List warranties -->
<route url="/warranty" method="get">
<service class="Vendor\WarrantyApi\Api\WarrantyRepositoryInterface" method="getList"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_view"/>
</resources>
</route>
<!-- Get warranty by ID -->
<route url="/warranty/:id" method="get">
<service class="Vendor\WarrantyApi\Api\WarrantyRepositoryInterface" method="getById"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_view"/>
</resources>
</route>
<!-- Create warranty -->
<route url="/warranty" method="post">
<service class="Vendor\WarrantyApi\Api\WarrantyRepositoryInterface" method="save"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_edit"/>
</resources>
</route>
<!-- Update warranty -->
<route url="/warranty/:id" method="put">
<service class="Vendor\WarrantyApi\Api\WarrantyRepositoryInterface" method="save"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_edit"/>
</resources>
</route>
<!-- Delete warranty -->
<route url="/warranty/:id" method="delete">
<service class="Vendor\WarrantyApi\Api\WarrantyRepositoryInterface" method="deleteById"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_edit"/>
</resources>
</route>
<!-- Get by order -->
<route url="/warranty/order/:incrementId" method="get">
<service class="Vendor\WarrantyApi\Api\WarrantyManagementInterface" method="getByOrderIncrementId"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_view"/>
</resources>
</route>
<!-- Claim warranty -->
<route url="/warranty/:id/claim" method="post">
<service class="Vendor\WarrantyApi\Api\WarrantyManagementInterface" method="claimWarranty"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_edit"/>
</resources>
</route>
<!-- Extend warranty -->
<route url="/warranty/:id/extend" method="post">
<service class="Vendor\WarrantyApi\Api\WarrantyManagementInterface" method="extendWarranty"/>
<resources>
<resource ref="Vendor_WarrantyApi::warranty_edit"/>
</resources>
</route>
</router>
</config>
ACL Configuration
<?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="Magento_Backend::stores">
<resource id="Magento_Backend::stores_settings">
<resource id="Magento_Config::config">
<resource id="Vendor_WarrantyApi::warranty" title="Warranty API"/>
</resource>
</resource>
</resource>
</resource>
<resource id="Vendor_WarrantyApi::api">
<resource id="Vendor_WarrantyApi::warranty_view" title="View Warranty"/>
<resource id="Vendor_WarrantyApi::warranty_edit" title="Edit Warranty"/>
</resource>
</resources>
</acl>
</config>
API Token Authentication
// Request with Bearer token:
// GET /rest/V1/warranty
// Authorization: Bearer <admin-token>
// Generate admin token:
// POST /rest/V1/integration/admin/token
// {"username": "admin", "password": "admin123"}
// Customer token:
// POST /rest/V1/integration/customer/token
// {"username": "customer@example.com", "password": "password123"}
// OAuth 1.0a:
// Configure in admin > System > Integrations
// Use consumer key/secret for request signing
Repository Implementation and Error Handling
Custom Exceptions
<?php
namespace Vendor\WarrantyApi\Exception;
use Magento\Framework\Exception\LocalizedException;
class CouldNotSaveException extends LocalizedException
{
}
class NoSuchEntityException extends \Magento\Framework\Exception\NoSuchEntityException
{
}
class InvalidInputException extends \Magento\Framework\Exception\CouldNotSaveException
{
}
Repository Implementation
<?php
namespace Vendor\WarrantyApi\Model;
use Vendor\WarrantyApi\Api\WarrantyRepositoryInterface;
use Vendor\WarrantyApi\Api\Data\WarrantyInterface;
use Vendor\WarrantyApi\Model\ResourceModel\Warranty as WarrantyResource;
use Vendor\WarrantyApi\Model\WarrantyFactory;
use Vendor\WarrantyApi\Exception\CouldNotSaveException;
use Vendor\WarrantyApi\Exception\NoSuchEntityException;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\Api\SearchResultsFactory;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
class WarrantyRepository implements WarrantyRepositoryInterface
{
public function __construct(
private WarrantyResource $resource,
private WarrantyFactory $warrantyFactory,
private SearchResultsFactory $searchResultsFactory,
private \Vendor\WarrantyApi\Model\ResourceModel\Warranty\CollectionFactory $collectionFactory,
private CollectionProcessorInterface $collectionProcessor,
) {
}
public function getById(int $id): WarrantyInterface
{
$warranty = $this->warrantyFactory->create();
$this->resource->load($warranty, $id);
if (!$warranty->getId()) {
throw new NoSuchEntityException(
__('Warranty with ID %1 not found', $id)
);
}
return $warranty;
}
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
{
$collection = $this->collectionFactory->create();
$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(WarrantyInterface $warranty): WarrantyInterface
{
try {
$this->resource->save($warranty);
} catch (\Exception $e) {
throw new CouldNotSaveException(
__('Could not save warranty: %1', $e->getMessage()),
$e
);
}
return $warranty;
}
public function delete(WarrantyInterface $warranty): bool
{
try {
$this->resource->delete($warranty);
} catch (\Exception $e) {
throw new \Magento\Framework\Exception\CouldNotDeleteException(
__('Could not delete warranty: %1', $e->getMessage()),
$e
);
}
return true;
}
public function deleteById(int $id): bool
{
return $this->delete($this->getById($id));
}
}
Management Implementation
<?php
namespace Vendor\WarrantyApi\Model;
use Vendor\WarrantyApi\Api\WarrantyManagementInterface;
use Vendor\WarrantyApi\Api\WarrantyRepositoryInterface;
use Vendor\WarrantyApi\Api\Data\WarrantyInterface;
use Vendor\WarrantyApi\Exception\NoSuchEntityException;
use Vendor\WarrantyApi\Exception\InvalidInputException;
class WarrantyManagement implements WarrantyManagementInterface
{
public function __construct(
private WarrantyRepositoryInterface $warrantyRepository,
) {
}
public function getByOrderIncrementId(string $incrementId): WarrantyInterface
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('order_increment_id', $incrementId)
->create();
$result = $this->warrantyRepository->getList($searchCriteria);
$items = $result->getItems();
if (empty($items)) {
throw new NoSuchEntityException(
__('No warranty found for order %1', $incrementId)
);
}
return reset($items);
}
public function claimWarranty(int $warrantyId): WarrantyInterface
{
$warranty = $this->warrantyRepository->getById($warrantyId);
if ($warranty->getStatus() !== WarrantyInterface::STATUS_ACTIVE) {
throw new InvalidInputException(
__('Only active warranties can be claimed')
);
}
if (new \DateTime($warranty->getExpiryDate()) < new \DateTime()) {
$warranty->setStatus(WarrantyInterface::STATUS_EXPIRED);
$this->warrantyRepository->save($warranty);
throw new InvalidInputException(
__('Warranty has expired on %1', $warranty->getExpiryDate())
);
}
$warranty->setStatus(WarrantyInterface::STATUS_CLAIMED);
return $this->warrantyRepository->save($warranty);
}
public function extendWarranty(int $warrantyId, string $newExpiryDate): WarrantyInterface
{
$warranty = $this->warrantyRepository->getById($warrantyId);
if ($warranty->getStatus() !== WarrantyInterface::STATUS_ACTIVE) {
throw new InvalidInputException(
__('Only active warranties can be extended')
);
}
$newDate = new \DateTime($newExpiryDate);
$currentDate = new \DateTime($warranty->getExpiryDate());
if ($newDate <= $currentDate) {
throw new InvalidInputException(
__('New expiry date must be after current expiry date')
);
}
$warranty->setExpiryDate($newExpiryDate);
return $this->warrantyRepository->save($warranty);
}
}
Testing and API Documentation
API Testing with cURL
# Get admin token
curl -X POST "https://magento.local/rest/V1/integration/admin/token" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}'
# Create warranty
curl -X POST "https://magento.local/rest/V1/warranty" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"warranty": {
"order_increment_id": "000000100",
"product_sku": "TSHIRT-001",
"customer_email": "customer@example.com",
"status": "active",
"expiry_date": "2027-12-31"
}
}'
# Get list with search criteria
curl -X GET "https://magento.local/rest/V1/warranty?searchCriteria[filterGroups][0][filters][0][field]=status&searchCriteria[filterGroups][0][filters][0][value]=active" \
-H "Authorization: Bearer <token>"
# Claim warranty
curl -X POST "https://magento.local/rest/V1/warranty/1/claim" \
-H "Authorization: Bearer <token>"
Error Response Format
{
"message": "Could not save warranty: SQL integrity constraint violation",
"parameters": []
}
{
"message": "Warranty with ID 999 not found",
"parameters": [
{
"fieldName": "id",
"fieldValue": 999
}
]
}
Integration Test
<?php
namespace Vendor\WarrantyApi\Test\Integration\Api;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
class WarrantyRepositoryTest extends TestCase
{
public function testCreateAndRetrieveWarranty(): void
{
$objectManager = Bootstrap::getObjectManager();
$warranty = $objectManager->create(
\Vendor\WarrantyApi\Api\Data\WarrantyInterface::class
);
$warranty->setOrderIncrementId('000000100');
$warranty->setProductSku('TSHIRT-001');
$warranty->setCustomerEmail('test@example.com');
$warranty->setStatus('active');
$warranty->setExpiryDate('2027-12-31');
$repository = $objectManager->create(
\Vendor\WarrantyApi\Api\WarrantyRepositoryInterface::class
);
$savedWarranty = $repository->save($warranty);
$this->assertNotNull($savedWarranty->getId());
$retrievedWarranty = $repository->getById($savedWarranty->getId());
$this->assertEquals('TSHIRT-001', $retrievedWarranty->getProductSku());
// Cleanup
$repository->delete($retrievedWarranty);
}
public function testGetListWithSearchCriteria(): void
{
$objectManager = Bootstrap::getObjectManager();
$searchCriteriaBuilder = $objectManager->create(
\Magento\Framework\Api\SearchCriteriaBuilder::class
);
$searchCriteria = $searchCriteriaBuilder
->addFilter('status', 'active')
->setPageSize(10)
->create();
$repository = $objectManager->create(
\Vendor\WarrantyApi\Api\WarrantyRepositoryInterface::class
);
$result = $repository->getList($searchCriteria);
$this->assertIsArray($result->getItems());
}
}
Quiz
1. Where are REST API routes defined in Magento 2?
2. How do you authenticate REST API requests?
3. What is the purpose of the Service Contract layer?
Flashcards
Question
Where are REST routes defined?
Click to reveal answer
Answer
etc/webapi.xml
Question
How do you generate an admin token?
Click to reveal answer
Answer
POST /rest/V1/integration/admin/token with username/password
Question
What interface must repository implement?
Click to reveal answer
Answer
Magento\Framework\Api\CrudRepositoryInterface or custom RepositoryInterface
Question
How do search criteria work?
Click to reveal answer
Answer
SearchCriteriaBuilder chains filters, sort orders, and pagination
Question
What HTTP methods map to CRUD?
Click to reveal answer
Answer
GET=read, POST=create, PUT=update, DELETE=delete
Revision Notes
Key Takeaways
- 1. webapi.xml defines REST routes mapped to service interfaces
- 2. Bearer token or OAuth 1.0a provides authentication
- 3. ACL resources control API access permissions
- 4. Repository pattern handles CRUD with proper error handling
- 5. Custom exceptions provide meaningful API error responses
Interview Tips
- • Explain the full lifecycle of a REST API request in Magento
- • Describe how authentication and ACL work together
- • Discuss error handling patterns for API endpoints
- • Talk about search criteria and filtering
Cheat Sheet
REST API Module:
etc/webapi.xml → Routes
etc/acl.xml → API permissions
Api/Interface.php → Service contract
Model/Repository.php → Implementation
HTTP Methods:
GET → List/Read
POST → Create
PUT → Update
DELETE → Delete
Authentication:
Bearer token → Admin/Customer tokens
OAuth 1.0a → Integration tokens
Error Handling:
NoSuchEntityException → 404
CouldNotSaveException → 400
LocalizedException → 500
Search Criteria:
addFilter(field, value, condition)
addSortOrder(field, direction)
setPageSize(n)
setCurrentPage(n)