Developer Journal

Intermediate · 12 minute read

Writing Custom Drupal Modules the Right Way

A complete, maintainable module pattern using routes, services, dependency injection, permissions, and forms.

Last updated August 6, 2026

A custom module should be small at its boundary and explicit about its dependencies. The goal is not merely code that runs; it is code another developer can discover, test, replace, and deploy safely.

Start with a narrow module contract

The .info.yml file declares compatibility and module dependencies. Routing maps an HTTP path to a controller. Permissions describe intent in language administrators can understand. Keep each file focused on one framework responsibility.

Put behavior in services

Controllers should translate a request into a response, not become application service containers. Define reusable behavior in services.yml, inject interfaces through the constructor, and let Drupal’s container assemble the object graph.

Constructor injection makes dependencies visible and allows unit tests to supply controlled collaborators. Avoid calling the global service locator from ordinary class methods.

Forms are workflows

A Form API class defines inputs, validation, and submission as separate steps. Validate business rules server-side, protect permissions at the route, and make side effects idempotent where retries are possible.

Working example

namespace Drupal\portfolio_tools\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\portfolio_tools\ReportBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

final class ReportController extends ControllerBase {
  public function __construct(private readonly ReportBuilderInterface $builder) {}
  public static function create(ContainerInterface $container): static {
    return new static($container->get('portfolio_tools.report_builder'));
  }
  public function view(): array {
    return ['#markup' => $this->builder->summary()];
  }
}

Key Takeaways

  • Keep controllers thin and business behavior in injected services.
  • Declare permissions and dependencies explicitly.
  • Design validation and side effects as separate concerns.

Further Reading