Everything in Magento is a module - the catalog, the checkout, the admin panel. Building your own is the foundational skill of Magento development, and the good news is the boilerplate, once learned, never changes. This guide builds a minimal but real module: a custom storefront page rendered by your own controller, block and template.
The Skeleton
Modules live in app/code/Vendor/ModuleName. The two mandatory files:
registration.php:
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Acme_StoreInfo',
__DIR__
);
etc/module.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<module name="Acme_StoreInfo"/>
</config>
Enable it and Magento knows the module exists:
bin/magento module:enable Acme_StoreInfo
bin/magento setup:upgrade
Adding a Frontend Route and Controller
etc/frontend/routes.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<router id="standard">
<route id="storeinfo" frontName="storeinfo">
<module name="Acme_StoreInfo"/>
</route>
</router>
</config>
Controller/Index/Index.php:
<?php
namespace Acme\StoreInfo\Controller\Index;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;
class Index implements HttpGetActionInterface
{
public function __construct(private PageFactory $pageFactory) {}
public function execute()
{
return $this->pageFactory->create();
}
}
This wires /storeinfo to a page. Implementing HttpGetActionInterface (rather than the old Action base class) keeps controllers thin and explicitly declares the HTTP verb - do this for every new controller.
Layout, Block and Template
view/frontend/layout/storeinfo_index_index.xml:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column">
<body>
<referenceContainer name="content">
<block class="Acme\StoreInfo\Block\Hours"
name="storeinfo.hours"
template="Acme_StoreInfo::hours.phtml"/>
</referenceContainer>
</body>
</page>
The block exposes methods, the template calls them. Keep logic in the block, presentation in the .phtml - the template should contain no database queries, ever.
Growing From Here
The same patterns extend to everything else: etc/di.xml for dependency injection configuration, Setup/Patch/Data for data migrations, etc/db_schema.xml for declarative tables (always prefer declarative schema over install scripts on modern Magento), view/adminhtml/ui_component for admin grids.
The habits that keep modules upgrade-safe: never edit core files, extend through plugins and observers rather than rewrites, declare PHP and Magento version compatibility in composer.json, and package the module with Composer from day one even if it only ever lives in one project.
A module is maybe thirty lines of boilerplate before it does something real. Learn the shape once and every feature you build - integrations, custom checkout steps, ERP syncs - is the same shape at a larger scale.