A comprehensive guide to understanding and implementing routing in Magento 2.
- Introduction
- How Routing Works
- Route Configuration
- Controllers
- URL Structure
- Route Parameters
- URL Rewriting
- Custom Routers
- Practical Examples
- Best Practices
Routing in Magento 2 is the mechanism that maps URLs to controller actions. When a user visits a URL, Magento's routing system:
- Parses the URL
- Identifies the appropriate router
- Finds the matching route
- Executes the corresponding controller action
- URL Structure - Creates clean, SEO-friendly URLs
- Module Organization - Maps URLs to specific modules
- Request Handling - Directs requests to appropriate controllers
- Customization - Allows overriding and extending default behavior
Magento 2 has different routers for different areas:
| Router | Area | ID | Purpose |
|---|---|---|---|
| Standard Router | Frontend | standard |
Public-facing pages |
| Admin Router | Backend | admin |
Admin panel pages |
| URL Rewrite Router | Both | N/A | SEO-friendly URLs |
| Default Router | Both | N/A | CMS pages, 404 handling |
User Request → FrontController → Router Collection → Match Router → Execute Controller
Step-by-Step Process:
- Request received - User visits URL like
/blog/post/view/id/5 - Front Controller -
Magento\Framework\App\FrontControllerreceives request - Router Collection - Loops through registered routers
- Router Matching - Each router tries to match the URL
- Route Found - Matching router processes the request
- Controller Execution - Controller action executes
- Response - Result is returned to user
Standard Magento 2 URL structure:
https://example.com/frontName/controllerName/actionName/param1/value1/param2/value2
Example:
https://example.com/catalog/product/view/id/123
↓ ↓ ↓ ↓ ↓
frontName controller action key value
Routers are executed in a specific order:
- Custom Routers (highest priority)
- Admin Router -
adminroutes - Standard Router -
standardroutes - URL Rewrite Router - SEO URLs
- CMS Router - CMS pages
- Default Router - 404 handling (lowest priority)
Routes are defined in routes.xml files located in your module's etc directory.
File: app/code/Vendor/Module/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="vendor_module" frontName="blog">
<module name="Vendor_Module" />
</route>
</router>
</config>URL Result: https://example.com/blog/...
File: app/code/Vendor/Module/etc/adminhtml/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="admin">
<route id="vendor_module" frontName="blog">
<module name="Vendor_Module" />
</route>
</router>
</config>URL Result: https://example.com/admin/blog/...
| Attribute | Description | Example |
|---|---|---|
id |
Unique route identifier | vendor_module |
frontName |
URL segment | blog |
module |
Module name | Vendor_Module |
before |
Load before specified module | before="Magento_Customer" |
after |
Load after specified module | after="Magento_Cms" |
<router id="standard">
<route id="catalog" frontName="catalog">
<module name="Magento_Catalog" />
<module name="Vendor_CatalogExtension" before="Magento_Catalog" />
</route>
</router>This allows Vendor_CatalogExtension to override Magento_Catalog controllers.
Controllers in Magento 2 follow a specific directory structure:
Vendor/Module/
└── Controller/
├── Index/
│ └── Index.php → /frontName/index/index
├── Post/
│ ├── View.php → /frontName/post/view
│ ├── Edit.php → /frontName/post/edit
│ └── Save.php → /frontName/post/save
└── Category/
└── List.php → /frontName/category/list
File: app/code/Vendor/Module/Controller/Index/Index.php
<?php
namespace Vendor\Module\Controller\Index;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action
{
protected $resultPageFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
// Return a page result
return $this->resultPageFactory->create();
}
}URL: https://example.com/blog/index/index or https://example.com/blog
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->set(__('Blog Posts'));
return $resultPage;
}protected $resultJsonFactory;
public function __construct(
Context $context,
\Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
) {
parent::__construct($context);
$this->resultJsonFactory = $resultJsonFactory;
}
public function execute()
{
$result = $this->resultJsonFactory->create();
return $result->setData(['success' => true, 'message' => 'Data saved']);
}protected $resultRedirectFactory;
public function execute()
{
$resultRedirect = $this->resultRedirectFactory->create();
$resultRedirect->setPath('*/*/index'); // Redirect to index action
return $resultRedirect;
}protected $resultForwardFactory;
public function __construct(
Context $context,
\Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory
) {
parent::__construct($context);
$this->resultForwardFactory = $resultForwardFactory;
}
public function execute()
{
$resultForward = $this->resultForwardFactory->create();
return $resultForward->forward('noroute'); // Forward to 404
}protected $resultRawFactory;
public function __construct(
Context $context,
\Magento\Framework\Controller\Result\RawFactory $resultRawFactory
) {
parent::__construct($context);
$this->resultRawFactory = $resultRawFactory;
}
public function execute()
{
$result = $this->resultRawFactory->create();
$result->setContents('Plain text response');
return $result;
}Admin controllers extend \Magento\Backend\App\Action and include ACL checking.
File: app/code/Vendor/Module/Controller/Adminhtml/Post/Index.php
<?php
namespace Vendor\Module\Controller\Adminhtml\Post;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Index extends Action
{
const ADMIN_RESOURCE = 'Vendor_Module::posts';
protected $resultPageFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
$resultPage = $this->resultPageFactory->create();
$resultPage->setActiveMenu('Vendor_Module::posts');
$resultPage->getConfig()->getTitle()->prepend(__('Manage Posts'));
return $resultPage;
}
}https://example.com/[frontName]/[controllerName]/[actionName]/[param1]/[value1]
Example: https://example.com/blog/post/view/id/5/page/2
| Component | Value | Source |
|---|---|---|
| Base URL | https://example.com |
Store configuration |
| Front Name | blog |
routes.xml |
| Controller | post |
Directory name |
| Action | view |
Class name |
| Parameters | id=5, page=2 |
URL params |
If controller or action is omitted, Magento uses index:
/blog → /blog/index/index
/blog/post → /blog/post/index
/blog/post/view → /blog/post/view
public function execute()
{
// Get single parameter
$id = $this->getRequest()->getParam('id');
// Get with default value
$page = $this->getRequest()->getParam('page', 1);
// Get all parameters
$params = $this->getRequest()->getParams();
// Get POST data
$postData = $this->getRequest()->getPostValue();
// Check request method
if ($this->getRequest()->isPost()) {
// Handle POST request
}
}public function execute()
{
$id = $this->getRequest()->getParam('id');
// Validate numeric
if (!is_numeric($id)) {
$this->messageManager->addErrorMessage(__('Invalid ID'));
return $this->resultRedirectFactory->create()->setPath('*/*/');
}
// Cast to integer
$id = (int) $id;
// Validate positive number
if ($id <= 0) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Invalid product ID')
);
}
}// Using URL Builder
$url = $this->_url->getUrl('blog/post/view', ['id' => 5, 'category' => 'tech']);
// Result: /blog/post/view/id/5/category/tech
// Using path only
$this->resultRedirectFactory->create()->setPath(
'blog/post/view',
['id' => 5]
);// In .phtml files
$url = $block->getUrl('blog/post/view', ['id' => 5]);
<!-- Or using escapeUrl -->
<a href="<?= $block->escapeUrl($block->getUrl('blog/post/view', ['id' => 5])) ?>">
View Post
</a><block class="Magento\Framework\View\Element\Html\Link">
<arguments>
<argument name="path" xsi:type="string">blog/post/view</argument>
<argument name="label" xsi:type="string" translate="true">View Posts</argument>
</arguments>
</block>URL rewrites create SEO-friendly URLs that map to standard Magento routes.
Before: /catalog/product/view/id/123
After: /my-awesome-product.html
<?php
namespace Vendor\Module\Model;
use Magento\UrlRewrite\Model\UrlRewriteFactory;
use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite;
class UrlRewriteCreator
{
protected $urlRewriteFactory;
protected $urlRewriteResource;
public function __construct(
UrlRewriteFactory $urlRewriteFactory,
UrlRewrite $urlRewriteResource
) {
$this->urlRewriteFactory = $urlRewriteFactory;
$this->urlRewriteResource = $urlRewriteResource;
}
public function createRewrite($postId)
{
$urlRewrite = $this->urlRewriteFactory->create();
$urlRewrite->setEntityType('custom')
->setEntityId($postId)
->setRequestPath('blog/my-post-slug')
->setTargetPath('blog/post/view/id/' . $postId)
->setRedirectType(0)
->setStoreId(1)
->setIsAutogenerated(0);
$this->urlRewriteResource->save($urlRewrite);
}
}| Redirect Type | Code | Description |
|---|---|---|
| No Redirect | 0 | Direct rewrite (no 301/302) |
| Permanent | 301 | Permanent redirect |
| Temporary | 302 | Temporary redirect |
Create custom routers when you need:
- Complex URL patterns
- Dynamic routing logic
- Integration with external systems
- Non-standard URL structures
File: app/code/Vendor/Module/Controller/Router.php
<?php
namespace Vendor\Module\Controller;
use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\RouterInterface;
class Router implements RouterInterface
{
protected $actionFactory;
public function __construct(ActionFactory $actionFactory)
{
$this->actionFactory = $actionFactory;
}
public function match(RequestInterface $request): ?ActionInterface
{
$identifier = trim($request->getPathInfo(), '/');
// Check if this router should handle the request
if (strpos($identifier, 'custom-route') !== 0) {
return null;
}
// Parse custom URL pattern
// Example: /custom-route/post-123
if (preg_match('#^custom-route/post-(\d+)$#', $identifier, $matches)) {
$postId = $matches[1];
// Set module, controller, action
$request->setModuleName('vendor_module')
->setControllerName('post')
->setActionName('view')
->setParam('id', $postId);
return $this->actionFactory->create(
\Magento\Framework\App\Action\Forward::class
);
}
return null;
}
}File: app/code/Vendor/Module/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="custom_router" xsi:type="array">
<item name="class" xsi:type="string">Vendor\Module\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">40</item>
</item>
</argument>
</arguments>
</type>
</config>Sort Order:
- Lower numbers = higher priority
- Standard router: 30
- Custom routers: 20-40
- CMS router: 60
Vendor/BlogModule/
├── etc/
│ ├── module.xml
│ └── frontend/
│ └── routes.xml
├── Controller/
│ ├── Index/
│ │ └── Index.php
│ ├── Post/
│ │ ├── View.php
│ │ └── List.php
│ └── Category/
│ └── View.php
└── registration.php
<?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="blog" frontName="blog">
<module name="Vendor_BlogModule" />
</route>
</router>
</config><?php
namespace Vendor\BlogModule\Controller\Post;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
use Vendor\BlogModule\Model\PostFactory;
class View extends Action
{
protected $resultPageFactory;
protected $postFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory,
PostFactory $postFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
$this->postFactory = $postFactory;
}
public function execute()
{
$postId = (int) $this->getRequest()->getParam('id');
if (!$postId) {
$this->messageManager->addErrorMessage(__('Post ID is required'));
return $this->resultRedirectFactory->create()->setPath('blog');
}
$post = $this->postFactory->create()->load($postId);
if (!$post->getId()) {
$this->messageManager->addErrorMessage(__('Post not found'));
return $this->resultRedirectFactory->create()->setPath('blog');
}
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->set($post->getTitle());
return $resultPage;
}
}URL: /blog/post/view/id/5
<?php
namespace Vendor\Module\Controller\Post;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
class Save extends Action
{
protected $jsonFactory;
protected $postRepository;
public function __construct(
Context $context,
JsonFactory $jsonFactory,
\Vendor\Module\Api\PostRepositoryInterface $postRepository
) {
parent::__construct($context);
$this->jsonFactory = $jsonFactory;
$this->postRepository = $postRepository;
}
public function execute()
{
$result = $this->jsonFactory->create();
if (!$this->getRequest()->isAjax()) {
return $result->setData([
'success' => false,
'message' => 'Invalid request'
]);
}
try {
$data = $this->getRequest()->getPostValue();
// Validate data
if (empty($data['title'])) {
throw new \Exception(__('Title is required'));
}
// Save post
$post = $this->postRepository->save($data);
return $result->setData([
'success' => true,
'message' => __('Post saved successfully'),
'post_id' => $post->getId()
]);
} catch (\Exception $e) {
return $result->setData([
'success' => false,
'message' => $e->getMessage()
]);
}
}
}<?php
namespace Vendor\Module\Controller\Adminhtml\Post;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class Edit extends Action
{
const ADMIN_RESOURCE = 'Vendor_Module::post_save';
protected $resultPageFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
protected function _isAllowed()
{
return $this->_authorization->isAllowed(self::ADMIN_RESOURCE);
}
public function execute()
{
$id = $this->getRequest()->getParam('id');
$resultPage = $this->resultPageFactory->create();
$resultPage->setActiveMenu('Vendor_Module::posts');
if ($id) {
$resultPage->getConfig()->getTitle()->prepend(__('Edit Post'));
} else {
$resultPage->getConfig()->getTitle()->prepend(__('New Post'));
}
return $resultPage;
}
}✅ Good:
<route id="vendor_modulename" frontName="modulename">❌ Bad:
<route id="mod" frontName="x">✅ Good Structure:
Controller/
├── Index/Index.php
├── Post/
│ ├── View.php
│ ├── Edit.php
│ └── Save.php
❌ Bad Structure:
Controller/
├── PostView.php
├── PostEdit.php
└── PostSave.php
✅ Always Validate:
$id = (int) $this->getRequest()->getParam('id');
if ($id <= 0) {
throw new \Magento\Framework\Exception\NoSuchEntityException();
}❌ Don't Trust Input:
$id = $this->getRequest()->getParam('id');
$model->load($id); // Dangerous!✅ Good:
public function __construct(
Context $context,
PostRepositoryInterface $postRepository
) {
parent::__construct($context);
$this->postRepository = $postRepository;
}❌ Bad:
public function execute()
{
$post = $this->_objectManager->create('Vendor\Module\Model\Post');
}Choose the right result type for your use case:
| Use Case | Result Type |
|---|---|
| Full page render | PageFactory |
| AJAX/API response | JsonFactory |
| Redirect | RedirectFactory |
| Plain text | RawFactory |
| Forward to another action | ForwardFactory |
✅ Use URL Builder:
$url = $this->_url->getUrl('blog/post/view', ['id' => $postId]);❌ Don't Hardcode:
$url = '/blog/post/view/id/' . $postId; // Bad!Always implement ACL checking:
const ADMIN_RESOURCE = 'Vendor_Module::resource_name';
protected function _isAllowed()
{
return $this->_authorization->isAllowed(self::ADMIN_RESOURCE);
}For public-facing pages, use URL rewrites:
// Instead of: /catalog/product/view/id/123
// Use: /my-product-name.htmlProblem: Route not working after creating routes.xml
Solution:
# Clear cache
bin/magento cache:clean
bin/magento cache:flush
# Recompile if needed
bin/magento setup:di:compileProblem: Controller exists but doesn't execute
Checklist:
- Check class namespace matches directory structure
- Verify
execute()method exists - Check routes.xml is in correct directory
- Clear cache
- Check file permissions
Problem: Admin controller returns 403
Solution:
// Ensure ACL resource is defined in acl.xml
// And user role has permission
const ADMIN_RESOURCE = 'Vendor_Module::posts';Problem: getParam() returns null
Solution:
// Check URL format
// Correct: /blog/post/view/id/5
// Wrong: /blog/post/view?id=5 (use getQuery() for query strings)
// Use getParam with default
$id = $this->getRequest()->getParam('id', 0);- Routes map URLs to controllers - Define in
routes.xml - Controllers handle requests - Follow naming conventions
- Parameters pass data - Validate all input
- Multiple result types - Choose appropriate response
- Custom routers for complex needs - Implement
RouterInterface - URL rewrites for SEO - Create friendly URLs
- Security matters - Always validate and check permissions
Create a route:
<router id="standard">
<route id="blog" frontName="blog">
<module name="Vendor_Module" />
</route>
</router>Create a controller:
namespace Vendor\Module\Controller\Post;
class View extends \Magento\Framework\App\Action\Action
{
public function execute()
{
return $this->resultPageFactory->create();
}
}Get URL:
$url = $this->_url->getUrl('blog/post/view', ['id' => 5]);- Day 6: Database Schema - Learn how to create and manage database tables
- Day 7: Models & Collections - Work with data models
Congratulations! You now understand Magento 2 routing and can create custom routes and controllers for your modules.