Creates a frontend controller action in Magento 2 for the storefront. Use when building custom frontend pages, AJAX endpoints, form submission handlers, or API-like endpoints for JavaScript.
This skill guides you through creating a frontend controller action in Adobe Commerce/Magento 2 (Mage-OS). Frontend controllers handle HTTP requests and return responses for the storefront area.
Always implement HTTP method-specific action interfaces:
HttpGetActionInterface - For GET requestsHttpPostActionInterface - For POST requestsAlways declare strict types at the top of controller files:
declare(strict_types=1);
Never use ObjectManager directly. Always inject dependencies via constructor.
Use result factories to return appropriate response types:
JsonFactory for JSON responsesPageFactory for full page responsesRedirectFactory for redirectsRawFactory for raw outputDefine your route configuration in etc/frontend/routes.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="yourmodule" frontName="yourmodule">
<module name="Vendor_ModuleName" />
</route>
</router>
</config>
URL Structure: https://yourdomain.com/{frontName}/{controller}/{action}
Create the controller directory:
app/code/Vendor/ModuleName/Controller/
└── ControllerName/
└── ActionName.php
Example: Controller/Custom/Search.php maps to URL: /yourmodule/custom/search
<?php
/**
* Copyright © [Year] [Your Company]
* All rights reserved.
*/
declare(strict_types=1);
namespace Vendor\ModuleName\Controller\ControllerName;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
class ActionName implements HttpGetActionInterface, HttpPostActionInterface
{
/**
* @var JsonFactory
*/
private JsonFactory $resultJsonFactory;
/**
* @var RequestInterface
*/
private RequestInterface $request;
/**
* Constructor
*
* @param JsonFactory $resultJsonFactory
* @param RequestInterface $request
*/
public function __construct(
JsonFactory $resultJsonFactory,
RequestInterface $request
) {
$this->resultJsonFactory = $resultJsonFactory;
$this->request = $request;
}
/**
* Execute action
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
// Get request parameters
$searchKey = $this->request->getParam('searchKey');
$page = (int)$this->request->getParam('page', 1);
$limit = (int)$this->request->getParam('limit', 10);
// Your business logic here
$data = [
'success' => true,
'message' => 'Action completed successfully',
'data' => [
'searchKey' => $searchKey,
'page' => $page,
'limit' => $limit
]
];
// Return JSON response
$resultJson = $this->resultJsonFactory->create();
return $resultJson->setData($data);
}
}
<?php
/**
* Copyright © [Year] [Your Company]
* All rights reserved.
*/
declare(strict_types=1);
namespace Vendor\ModuleName\Controller\ControllerName;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\View\Result\Page;
class ActionName implements HttpGetActionInterface
{
/**
* @var PageFactory
*/
private PageFactory $resultPageFactory;
/**
* Constructor
*
* @param PageFactory $resultPageFactory
*/
public function __construct(
PageFactory $resultPageFactory
) {
$this->resultPageFactory = $resultPageFactory;
}
/**
* Execute action
*
* @return Page
*/
public function execute(): Page
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->set(__('Page Title'));
return $resultPage;
}
}
<?php
/**
* Copyright © [Year] [Your Company]
* All rights reserved.
*/
declare(strict_types=1);
namespace Vendor\ModuleName\Controller\ControllerName;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Controller\Result\RedirectFactory;
use Magento\Framework\Controller\ResultInterface;
class ActionName implements HttpGetActionInterface
{
/**
* @var RedirectFactory
*/
private RedirectFactory $resultRedirectFactory;
/**
* Constructor
*
* @param RedirectFactory $resultRedirectFactory
*/
public function __construct(
RedirectFactory $resultRedirectFactory
) {
$this->resultRedirectFactory = $resultRedirectFactory;
}
/**
* Execute action
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
$resultRedirect = $this->resultRedirectFactory->create();
$resultRedirect->setPath('customer/account');
return $resultRedirect;
}
}
If returning a page, create layout XML: view/frontend/layout/yourmodule_controllername_actionname.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<title>Page Title</title>
</head>
<body>
<referenceContainer name="content">
<block class="Vendor\ModuleName\Block\CustomBlock"
name="custom.block"
template="Vendor_ModuleName::custom/template.phtml"/>
</referenceContainer>
</body>
</page>
Create template file: view/frontend/templates/custom/template.phtml
<?php
/**
* @var \Magento\Framework\View\Element\Template $block
* @var \Magento\Framework\Escaper $escaper
*/
?>
<div class="custom-content">
<h1><?= $escaper->escapeHtml(__('Custom Page')) ?></h1>
<p><?= $escaper->escapeHtml(__('Your content here')) ?></p>
</div>
# Clear cache
ddev exec bin/magento cache:flush
# Upgrade setup (if new module)
ddev exec bin/magento setup:upgrade
# Test the endpoint
curl https://ntotank.ddev.site/yourmodule/controllername/actionname
public function execute(): ResultInterface
{
$searchKey = $this->request->getParam('searchKey');
// Load collection
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('name', ['like' => "%{$searchKey}%"]);
$collection->setPageSize(10);
// Format results
$results = [];
foreach ($collection as $item) {
$results[] = [
'id' => $item->getId(),
'name' => $item->getName(),
'url' => $item->getUrl()
];
}
$resultJson = $this->resultJsonFactory->create();
return $resultJson->setData([
'items' => $results,
'total' => $collection->getSize()
]);
}
public function execute(): ResultInterface
{
if (!$this->request->isPost()) {
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('*/*/');
}
try {
// Validate CSRF token (automatically done by Magento)
$formData = $this->request->getPostValue();
// Process form data
// ... your logic here
$this->messageManager->addSuccessMessage(__('Form submitted successfully.'));
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('*/*/success');
} catch (\Exception $e) {
$this->messageManager->addErrorMessage($e->getMessage());
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('*/*/');
}
}
private \Magento\Customer\Model\Session $customerSession;
public function execute(): ResultInterface
{
if (!$this->customerSession->isLoggedIn()) {
$resultRedirect = $this->resultRedirectFactory->create();
$resultRedirect->setPath('customer/account/login');
return $resultRedirect;
}
// Continue with authenticated logic
// ...
}
Create: Test/Unit/Controller/ControllerName/ActionNameTest.php
<?php
declare(strict_types=1);
namespace Vendor\ModuleName\Test\Unit\Controller\ControllerName;
use PHPUnit\Framework\TestCase;
use Vendor\ModuleName\Controller\ControllerName\ActionName;
class ActionNameTest extends TestCase
{
public function testExecuteReturnsJsonResult(): void
{
// Setup mocks
$resultJsonFactory = $this->createMock(\Magento\Framework\Controller\Result\JsonFactory::class);
$request = $this->createMock(\Magento\Framework\App\RequestInterface::class);
// Create controller instance
$controller = new ActionName($resultJsonFactory, $request);
// Execute and assert
$result = $controller->execute();
$this->assertInstanceOf(\Magento\Framework\Controller\ResultInterface::class, $result);
}
}
routes.xml is in correct location (etc/frontend/routes.xml)ddev exec bin/magento setup:upgradeddev exec bin/magento cache:flushProxiBlue_SearchDynaTablex-data and fetch() callsddev exec prefix for Magento CLI commands