Introduction to Drupal Custom Module Develpmentfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Think of a fresh Drupal install as an empty house with excellent plumbing, wiring, and structural bones — but no furniture. Contributed modules from drupal.org are like buying furniture off the shelf: fast, but not always the exact shape your project needs. A custom module is what you build yourself when the shelf doesn't have what you're looking for: a bespoke page, a unique integration, a rule specific to your business. This lesson walks you from a completely empty folder to a real, working module you can visit in a browser.

What you'll learn in this lesson

  • Why you'd write a custom module instead of reaching for a contributed one
  • What you need installed before you start (and why each tool matters)
  • The handful of files every module is built from, and what each one is responsible for
  • How to build and enable a real, working "Hello World" module from scratch
Drupal Custom Module developement tutorial by drupalzone

Why write a custom module at all?

Four reasons come up constantly in real projects:

  • Customization — a workflow or integration specific enough to your project that no contributed module fits it out of the box.
  • Reusability — package the logic once, drop it into every site that needs it, or share it with the community on drupal.org.
  • Performance — a lightweight, purpose-built module beats installing three heavyweight contributed modules just to get one feature.
  • Understanding — writing modules is how you actually learn Drupal's hooks, services, and render pipeline, rather than just configuring someone else's.

Prerequisites

Before you write a line of code, make sure you have:

  • Basic PHP (object-oriented style is what modern Drupal expects) and enough YAML to read a config file comfortably.
  • A local Drupal 10 or 11 site — DDEV, Lando, or any equivalent stack works fine.
  • Composer, and ideally Drush, available on the command line.
  • A code editor and Git — you don't strictly need Git to follow this lesson, but you'll want it the moment this module is worth keeping.

No prior module-building experience is assumed — this is genuinely the starting line.

The shape every module takes

A Drupal module is just a folder living under /modules/custom/ (custom, project-specific code) or /modules/contrib/ (code you downloaded from someone else). Drupal doesn't care what's inside until it finds one required file:

  • *.info.yml — the only file that's truly mandatory. It's how Drupal discovers your module exists at all, and declares its name, description, and core version compatibility.
  • *.module — an optional procedural PHP file, traditionally where hook implementations live.
  • *.routing.yml — optional; defines URL paths and which controller handles each one.
  • src/ — optional; holds PHP classes (controllers, services, plugins) following PSR-4 autoloading.
  • config/, templates/, css/, js/ — optional folders for default configuration, Twig overrides, and front-end assets.
Drupal Custom Module Development Basic File structure
Only one file is required: a module with nothing but a valid *.info.yml is a complete, installable Drupal module — it just won't do anything yet. Every other file on this list only exists because you added a capability that needs it.

Building "Hello World" step by step

The classic first module: visiting /hello should print "It works!". Small enough to type from scratch, but it touches every piece you'll use in every module after this one — an info file, a route, and a controller.

Step 1 — create the module folder

Inside your site's modules/custom/ directory, create a folder named hello_world. Stick to lowercase letters and underscores — this becomes your module's machine name, and it shows up in namespaces, service names, and hook function names for the rest of the module's life.

Step 2 — declare the module: hello_world.info.yml

name: 'Hello World'
type: module
description: 'This is a Hello world module'
package: Custom
core_version_requirement: ^10 || ^11

This is the file that registers your module with Drupal — the moment it exists with valid YAML, "Hello World" appears in the module list at /admin/modules, ready to be enabled.

Step 3 — define a route: hello_world.routing.yml

hello_world.hello_world_page:
  path: '/hello'
  defaults:
    _title: 'Hello World Page'
    _controller: '\Drupal\hello_world\Controller\HelloWorldPage'
  requirements:
    _permission: 'access content'

This tells Drupal's router: "when someone requests /hello, and they have the access content permission, hand the request to HelloWorldPage." No route, no page — this file is what turns a URL into a page at all.

Step 4 — write the controller

Create src/Controller/HelloWorldPage.php (the path under src/ must mirror the class's namespace, per PSR-4):

<?php

declare(strict_types=1);

namespace Drupal\hello_world\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Returns responses for Hello World routes.
 */
final class HelloWorldPage extends ControllerBase {

  /**
   * Builds the response.
   */
  public function __invoke(): array {

    $build['content'] = [
      '#type' => 'item',
      '#markup' => $this->t('It works!'),
    ];

    return $build;
  }

}

Notice this returns a render array, not a raw HTML string — '#markup' => $this->t('It works!') is Drupal's way of saying "here's a translatable string of content," and Drupal's rendering system takes care of turning it into a full HTML page around your site's theme.

Drupal custom Controller

Step 5 — enable and verify

  • Visit /admin/modules, search "Hello World," check the box, and click Install — or from the command line: drush en hello_world -y.
  • If anything looks stale, clear the cache: drush cr.
installing drupal module

Now visit /hello — you should see "It works!" on the page.

Drupal Controller route
If you get a 404 instead: the single most common cause is a stale route cache — the router only learns about new routes after a cache rebuild. Run drush cr and try again before suspecting a typo in the YAML.

Quick check: why does the controller return an array like ['#type' => 'item', '#markup' => ...] instead of just return "It works!";? (Answer: Drupal's rendering pipeline needs a render array so it can apply theming, caching metadata, and let other modules alter the output before it becomes final HTML — a raw string skips all of that machinery.)

Key takeaways

  • The only file a module truly requires is a valid *.info.yml — everything else exists only because you need that specific capability.
  • A module's machine name (its folder name) shows up everywhere: namespaces, hook function prefixes, service IDs — choose it carefully and keep it lowercase with underscores.
  • *.routing.yml maps a URL path to a controller; the controller's job is to return a render array, never raw HTML.
  • Whenever a new route or hook doesn't seem to be taking effect, clear the cache (drush cr) before assuming your code is wrong.

Coming up next

You've seen the .info.yml file in passing — next, we slow down and go through every key it supports, including the ones this lesson didn't need, in Module Info.yml File.