Building a custom module for Joomla! 5.x and beyond is more straightforward than the sprawling, often contradictory documentation on the internet might suggest. Here's a practical walkthrough — written for 2026 — that cuts through the noise and gets you to a working module quickly.
TL:DR – Most Joomla tutorials online are years out of date and target older versions. The single most reliable source remains the Official Joomla Module Development Tutorial, now maintained under the Joomla Programmers Documentation (currently at version 6.1 of the manual). Everything below is based on that resource, adapted for Joomla 5.x, with my own notes and observations layered on top.
Contents
A module called mod_modulename
The goal here is a clean skeleton — something you can copy and adapt whenever you need a new module. Once it's working, replacing the placeholder names and adding real logic is trivial.
Folder Structure
Joomla requires a specific folder layout. Deviate from it and the installer will either fail silently or produce confusing errors. The minimal structure looks like this:
mod_modulename
├── mod_modulename.xml
├── services
│ └── provider.php
└── src
└── Dispatcher
└── Dispatcher.php
For anything beyond a trivial module you'll likely add a tmpl folder for templates and a Helper folder under src, but the three files above are the absolute minimum to get Joomla to recognise and load your extension.
Manifest file
<?xml version="1.0" encoding="UTF-8"?>
<extension type="module" client="site" method="upgrade">
<name>Module name</name>
<version>1.0</version>
<author>Your Name</author>
<creationDate>2026-01-01</creationDate>
<description>Your first Joomla module</description>
<namespace path="src">YourCompanyName\Module\ModuleName</namespace>
<files>
<folder module="mod_modulename">services</folder>
<folder>src</folder>
</files>
</extension>
The manifest is the installer's source of truth for your extension. A few things worth understanding:
<extension type="module" client="site" method="upgrade">— declares this as a front-end module. Swapclient="site"forclient="administrator"if you're targeting the back end. Themethod="upgrade"attribute means reinstalling over an existing version won't wipe your settings.<name>,<author>,<creationDate>,<description>— plain text fields surfaced in the Joomla Extensions manager.<version>— semantic versioning is recommended; Joomla uses this when deciding whether an upgrade is needed.<files>— lists every folder the installer should treat as part of the module. Themodule="mod_modulename"attribute on theservicesfolder entry tells Joomla where to find the service provider, which is the module's entry point.<namespace>— the PSR-4 namespace prefix for your module's classes. Using a vendor prefix likeYourCompanyName\Module\ModuleNameavoids collisions with other extensions, even though there's no central registry enforcing uniqueness.
Service Provider
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\Module as ModuleServiceProvider;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory as ModuleDispatcherFactoryServiceProvider;
use Joomla\CMS\Extension\Service\Provider\HelperFactory as HelperFactoryServiceProvider;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactoryServiceProvider('\\YourCompanyName\\Module\\ModuleName'));
$container->registerServiceProvider(new HelperFactoryServiceProvider('\\YourCompanyName\\Module\\ModuleName\\Site\\Helper'));
$container->registerServiceProvider(new ModuleServiceProvider());
}
};
If you're new to Joomla development then this code probably looks very intimidating. If so, the best thing is just to accept it for now.
Fair enough. The service provider wires up Joomla's dependency injection container so the framework knows how to instantiate your Dispatcher and any Helper classes. The namespace strings passed to the factory constructors must match exactly what you declared in the manifest — a mismatch here is one of the most common reasons a freshly installed module produces a blank output with no obvious error.
Dispatcher
<?php
namespace YourCompanyName\Module\ModuleName\Site\Dispatcher;
\defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\DispatcherInterface;
class Dispatcher implements DispatcherInterface
{
public function dispatch(): void
{
echo '<h4>Hello, World</h4>';
}
}
Joomla instantiates your Dispatcher class and calls dispatch(). For a real module you'll want to move output into a template file under tmpl/ rather than echoing directly — it keeps logic and presentation separate and makes overrides in your template far easier to manage. Note the added return type hint : void on dispatch(), which aligns with the interface definition in current Joomla versions and avoids deprecation notices under strict PHP 8.x configurations.
Joomla 5.x requires PHP 8.1 as a minimum, and the codebase makes increasing use of PHP 8 features — named arguments, enums, readonly properties — so it's worth writing your module code with that baseline in mind from the start rather than retrofitting it later.
Install your module
- Zip the folder: % zip -r mod_modulename.zip mod_modulename/
- In the Joomla admin panel, go to System > Install > Extensions and upload the zip.


Enabling your new module
- Navigate to Content > Site Modules and find your newly installed module in the list.
- It will be installed but unpublished — you need to open it and set its status to Published.
- Assign it to a module position. If you're working with a custom template that doesn't advertise positions, you can type a position name directly and reference it in your template with
<jdoc:include type="modules" name="your-position" />. - Use the Menu Assignment tab to control which pages the module appears on.

The module in action
If everything is wired up correctly you should see the output of your module below:
Automated testing with Cypress
The official Joomla module tutorial ships with Cypress end-to-end tests, which is a genuinely useful practice to adopt. Running them gives you confidence that a reinstall or Joomla core update hasn't silently broken your module's output. The test suite covers installing the extension, verifying it appears in the administrator back end, and confirming it renders correctly on the front end.
npx cypress run
It looks like this is your first time using Cypress: 13.13.2
✔ Verified Cypress! ..../Cypress.app
Opening Cypress...
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 13.13.2 │
│ Browser: Electron 118 (headless) │
│ Node Version: v22.5.1 (/opt/homebrew/Cellar/node/22.5.1/bin/node) │
│ Specs: 1 found (tests.cy.js) │
│ Searched: cypress/integration/tests.cy.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
Running: tests.cy.js (1 of 1)
Testing 'Joomla module tutorial' from '../module-tutorial'
Testing 'step1_basic_module'
✓ Uninstall extension, if existing (2885ms)
✓ Install extension 'Joomla module tutorial' (10618ms)
✓ Check module exists in administrator backend (291ms)
✓ Check module outputs on the frontend website (1801ms)
Check that your Cypress version is current before running — the Cypress team ships breaking changes to configuration and folder conventions periodically, and tests written against older versions may need minor adjustments to run cleanly. The test structure above still holds, but verify your cypress.config.js matches the version you have installed.
Where to go from here
This skeleton is deliberately minimal. A production module will typically add a tmpl/default.php layout file, a Helper class to encapsulate business logic, language files under language/ for internationalisation, and potentially custom fields exposed through the manifest's <config> block. The Joomla Programmers Documentation walks through each of those additions in subsequent tutorial steps — and unlike much of what surfaces in search results, it's actively maintained and written against current Joomla releases.