Drupal

Writing a custom Drupal module that other people can maintain

Custom modules are where Drupal projects accumulate their long-term cost. A few conventions keep them readable and upgradeable.

Key takeaways

  • Check whether a contributed module or a content-model fix solves it first.
  • Use Drupal's service APIs; reaching around them is what breaks at major versions.
  • Configuration, state and settings are three different things — keep them apart.
  • Run PHPStan for deprecations so upgrades become a task list, not an investigation.

Decide whether you need one

The first question is whether a contributed module already solves this. The second is whether the requirement is genuine or a workaround for a content-model problem. A surprising share of custom modules exist to patch over decisions that would be cheaper to fix upstream.

When custom code is the right answer, keep the module narrow and named for what it does rather than for the project or the client.

Use the APIs, not the shortcuts

Drupal provides services for almost everything a module needs: entity queries, the database abstraction layer, the cache backend, the queue system, configuration and state storage. Reaching around them — raw SQL against core tables, direct file writes, global state — is what makes a module fail at the next major version.

Inject dependencies through the service container rather than calling \Drupal::service() throughout. It is marginally more code and makes the module testable.

Configuration, state and settings

These three are distinct and are regularly confused. Configuration is deployable and belongs in version control. State is runtime data that does not deploy, such as a last-run timestamp. Settings are per-environment values such as credentials, which belong in settings.php or environment variables, never in exported configuration.

Getting this wrong is how API keys end up committed to a repository.

Tests and deprecations

Write at least a kernel test for the module's core behaviour. It does not have to be exhaustive; it has to catch the case where a future upgrade silently changes something.

Run PHPStan with the Drupal extension in your pipeline. It reports deprecated API usage early, which converts the next major upgrade from an investigation into a task list.

Keeping modules small and named honestly

The most common structural mistake is the catch-all module — mysite_custom — that accumulates every piece of bespoke behaviour on the site. After two years it contains form alters, a block, three services, a migration and some theme preprocessing, and nobody can safely remove any of it.

Split by capability instead. A module that provides an integration with one external system, a module that provides one content feature, a module that provides shared utilities. Each has a clear reason to exist and can be removed when that reason expires.

Name modules for what they do rather than for the project or client. zbd_events tells the next developer nothing; event_registration tells them everything they need to decide whether to open the file.


Frequently asked questions

Usually yes, under web/modules/custom , versioned with the site. Splitting a module into its own repository is worth it only when it is genuinely reused across several projects, because the overhead of separate versioning and release management is real.

Enough to catch silent breakage at the next upgrade. A kernel test covering the module's central behaviour is usually the highest-value first test. Exhaustive unit coverage of glue code rarely repays the effort.

Yes, when the patch comes from the module's issue queue and is applied through Composer so it is recorded and re-applied on every build. Editing files in place inside the vendor directory is not acceptable, because the change disappears at the next update.

Working on this?

A conversation with our team, no charge and no obligation.

Book a conversation