A controller handles a request to a URL and returns a result: a page, a redirect, JSON or raw output. Since Magento 2.3 controllers implement HTTP method interfaces instead of extending the old Action base class, and that is the pattern we use here.
We assume you already have a module called MageServices_Hello. If not, follow our guide to creating a Magento 2 module first.
How Magento Maps URLs to Controllers
A storefront URL like /hello/greeting/show has three parts: the front name (hello), the controller folder (greeting) and the action class (show). Magento resolves it to MageServices\Hello\Controller\Greeting\Show. Missing parts default to index.
Step 1: Declare a Frontend Route
<?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="hello" frontName="hello">
<module name="MageServices_Hello"/>
</route>
</router>
</config>
Step 2: A GET Controller That Returns a Page
<?php
declare(strict_types=1);
namespace MageServices\Hello\Controller\Greeting;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;
class Show implements HttpGetActionInterface
{
public function __construct(
private readonly PageFactory $pageFactory
) {
}
public function execute(): Page
{
$page = $this->pageFactory->create();
$page->getConfig()->getTitle()->set(__('Greeting'));
return $page;
}
}
The page uses the layout handle hello_greeting_show, so create view/frontend/layout/hello_greeting_show.xml to add blocks to it.
Step 3: Read Request Parameters Safely
Inject RequestInterface and cast parameters to the type you expect. Never trust request input.
<?php
declare(strict_types=1);
namespace MageServices\Hello\Controller\Greeting;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;
class Lookup implements HttpGetActionInterface
{
public function __construct(
private readonly RequestInterface $request,
private readonly JsonFactory $jsonFactory
) {
}
public function execute(): Json
{
$name = trim((string) $this->request->getParam('name', ''));
$result = $this->jsonFactory->create();
if ($name === '' || mb_strlen($name) > 50) {
return $result->setHttpResponseCode(400)->setData(['error' => (string) __('Invalid name.')]);
}
return $result->setData(['message' => (string) __('Hello, %1!', $name)]);
}
}
Requesting /hello/greeting/lookup?name=Anna now returns {"message":"Hello, Anna!"}.
Step 4: A POST Controller With Form Key Validation
For actions that change data, implement HttpPostActionInterface. Magento validates the form key on POST requests to storefront controllers automatically, so your form must include it. Return a redirect after processing so a page refresh does not resubmit the form.
<?php
declare(strict_types=1);
namespace MageServices\Hello\Controller\Greeting;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\Redirect;
use Magento\Framework\Controller\Result\RedirectFactory;
use Magento\Framework\Message\ManagerInterface;
class Save implements HttpPostActionInterface
{
public function __construct(
private readonly RequestInterface $request,
private readonly RedirectFactory $redirectFactory,
private readonly ManagerInterface $messageManager
) {
}
public function execute(): Redirect
{
$message = trim((string) $this->request->getParam('message', ''));
if ($message === '') {
$this->messageManager->addErrorMessage(__('Please enter a message.'));
} else {
// Save the message through a repository or service class here.
$this->messageManager->addSuccessMessage(__('Thank you, your message was saved.'));
}
return $this->redirectFactory->create()->setPath('hello/greeting/show');
}
}
The form in your template needs the form key block output:
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Framework\View\Element\Template $block */
?>
<form method="post" action="<?= $escaper->escapeUrl($block->getUrl('hello/greeting/save')) ?>">
<?= $block->getBlockHtml('formkey') ?>
<label for="hello-message"><?= $escaper->escapeHtml(__('Message')) ?></label>
<input id="hello-message" name="message" type="text" maxlength="255" required>
<button type="submit"><?= $escaper->escapeHtml(__('Send')) ?></button>
</form>
Step 5: An Admin Controller
Admin controllers live under Controller/Adminhtml, use an adminhtml route and must declare an ACL resource so only authorised users can open them.
<?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="admin">
<route id="mageservices_hello" frontName="mageservices_hello">
<module name="MageServices_Hello"/>
</route>
</router>
</config>
<?php
declare(strict_types=1);
namespace MageServices\Hello\Controller\Adminhtml\Dashboard;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action implements HttpGetActionInterface
{
public const ADMIN_RESOURCE = 'MageServices_Hello::dashboard';
public function __construct(
Context $context,
private readonly PageFactory $pageFactory
) {
parent::__construct($context);
}
public function execute(): Page
{
$page = $this->pageFactory->create();
$page->getConfig()->getTitle()->prepend(__('Hello Dashboard'));
return $page;
}
}
Define MageServices_Hello::dashboard in etc/acl.xml so administrators can grant it to roles. Admin controllers still extend Magento\Backend\App\Action, which provides the ACL check and admin URL handling.
Result Types at a Glance
| Result | Factory | Use it for |
|---|---|---|
| Page | PageFactory | Full HTML pages rendered from layout XML |
| JSON | JsonFactory | AJAX endpoints used by your storefront JavaScript |
| Redirect | RedirectFactory | After POST actions, or to send users elsewhere |
| Forward | ForwardFactory | Internal forward to another action, such as a 404 |
| Raw | RawFactory | Plain text, XML or files you generate yourself |
For data that other systems or headless frontends consume, prefer a REST endpoint or GraphQL query over a custom JSON controller. They get authentication, schema and documentation for free.
Frequently Asked Questions
Is \Magento\Framework\App\Action\Action deprecated?
Yes. Storefront controllers should implement HttpGetActionInterface, HttpPostActionInterface or another HTTP method interface instead of extending the old Action class.
Why does my POST controller redirect without doing anything?
The form key is probably missing or invalid. Include the formkey block in your form so Magento's CSRF validation passes.
How do I restrict an admin controller?
Set the ADMIN_RESOURCE constant to an ACL resource defined in etc/acl.xml.
Need help with this on your store?
Our Magento engineers can implement it for you, review your code or take on the whole project.
Written by the Magento Services engineering team — Magento 2, Adobe Commerce and Hyvä specialists since 2014. We write about problems we solve on real client stores.