Modules, Routes and Controllers

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Creating a Module

Let's do something fun, like create a custom page. Like always, any custom code will live in a module. And modules live in the modules/ directory. Create a new one called dino_roar. To make Drupal fall in love with your module, create the info file: If you loved the old .info files, then you'll feel all warm and fuzzy with these: it's the same thing, but now in the YAML format.

Inside give it a name: Dino ROAR, a type: module, description: Roar at you, package: Sample and core: 8.x:

name: Dino ROAR
type: module
description: "ROOOOOAR at you"
package: Sample
core: 8.x

If YAML is new to you, cool! It's pretty underwhelming: just a colon separated key-value pair. But make sure you have at least one space after the colon. Yaml also supports hierarchies of data via indentation - but there's none of that in this file.

Module ready! Head back to the browser and go into the "Extend" section. With any luck we'll see the module here. There it is under "Sample": "Dino ROAR". It sounds terrifying. Check the box and press the install button anyways. What's the worst that could happen?

Nothing! But now we can build that page I keep talking about.

Add a Route

In any modern framework - and I am including Drupal in this category, yay! - creating a page is two steps. First, define the URL for the page via a route. That's your first buzzword in case you're writing things down.

Second, create a controller for that page. This is a function that you'll write that actually builds the page. It's also another buzzword: controller.

If these are new buzzwords for you, that's ok - they're just a new spin on some old ideas.

For step 1, create a new file in the module: dino_roar.routing.yml. Create a new route called dino_says: this is the internal name of the route and it isn't important yet:

... lines 2 - 7

Go in 4 spaces - or 2 spaces, it doesn't matter, just be consistent - and add a new property to this route called path. Set it to /the/dino/says: the URL to the new page:

path: /the/dino/says
... lines 3 - 7

Below path, a few more route properties are needed. The first, is defaults, with a _controller key beneath it:

path: /the/dino/says
_controller: Drupal\dino_roar\Controller\RoarController::roar
... lines 5 - 7

The _controller key tells Drupal which function should be called when someone goes to the URL for this exciting page. Set this to Drupal\dino_roar\Controller\RoarController::roar. This is a namespaced class followed by :: and then a method name. We'll create this function in a second.

Also add a requirements key with a _permission key set to access content:

... lines 2 - 4
_permission: 'access content'

We won't talk about permissions now, but this is what will allow us to view the page.

In YAML, you usually don't need quotes, except in some edge cases with special characters. But it's always safe to surround values with quotes. So if you're in doubt, use quotes! I don't need them around access content... but it makes me fee good.

Add a Controller Function

Step 1 complete: we have a route. For Step 2, we need to create the controller: the function that will actually build the page. Inside of the dino_roar module create an src directory and then a Controller directory inside of that. Finally, add a new PHP class called RoarController:

... lines 2 - 14

Ok, stop! Fun fact: every class you create will have a namespace at the top. If you're not comfortable with namespaces, they're really easy. So easy that we teach them to you in 120 seconds in our namespaces tutorial. So pause this video, check that out and then everything we're about to do will seem much more awesome.

But you can't just set the namespace to any old thing: there are rules. It must start with Drupal\, then the name of the module - dino_roar\, then whatever directory or directories this file lives in after src/. This class lives in Controller. Your class name also has to match the filename, + .php:

namespace Drupal\dino_roar\Controller;
... lines 4 - 6
class RoarController
... lines 9 - 12

If you mess any of this up, Drupal isn't going to be able to find your class.

The full class name is now Drupal\dino_roar\Controller\RoarController. Hey, this conveniently matches the _controller of our route!

In RoarController, add the new public function roar():

... lines 1 - 6
class RoarController
public function roar()
... line 11

A Controller Returns a Response

Now, you might be asking yourself what a controller function like this should return. And to that I say - excellent question! Brilliant! A controller should always return a Symfony Response object. Ok, that's not 100% true - but let me lie for just a little bit longer.


The code-styling (4 spaces indentation, etc) I'm using is called PSR-2. It's a great PHP standard, but is (I admit) different than the recommended Drupal standard.

To return a response, say return new Response(). I'll let it autocomplete the Response class from Symfony's HttpFoundation namespace. When I hit tab to select this, PhpStorm adds the use statement to the top of the class automatically:

... lines 1 - 4
use Symfony\Component\HttpFoundation\Response;
class RoarController
public function roar()
return new Response('ROOOOOAR!');

That's important: whenever you reference a class, you must add a use statement for it. If you forget, you'll get the famous "Class Not Found" error.

For the page content, we will of course ROOOAR!.

That's it! That's everything. Go to your browser and head to /the/dino/says:


Hmm page not found. As a seasoned Drupal developer, you may be wondering, "uhh do I need to clear some cache?" My gosh, you're right!

Leave a comment!

  • 2020-07-03 Victor Bocharsky

    Hey MonkeyBrain,

    Thank you for this tip!


  • 2020-07-02 MonkeyBrain

    I'm running this course on Drupal 9 (I know).
    It works so far ...
    In case anyone else is doing this, when updating you need to change
    core: 8.x
    core_version_requirement: ^8 || ^9

  • 2019-04-16 Victor Bocharsky

    Hey Viktor,

    Ah, the question is why do you need *Json* response? :) Json responses usually are used for API endpoints, like when users send AJAX request and server returns AJAX response back, so you can then use the information in the response e.g. put it on the page or just update the page with new information, etc. But if you want to render a template - then you probably don't want JsonResponse, I probably misunderstood your first question.

    I think to render a template instead you can do something like this:

    $rendered = \Drupal::service('renderer')->render([
    '#theme' => 'my_template',
    '#test_var' => 'test variable',

    return new Response($rendered);

    And I suppose your template name should be "my-template.html.twig" in this case.


  • 2019-04-15 Viktor

    I'm sorry, one more question. If you are returning json_Response. You data is already in json:

    For instance,

    return new JsonResponse(
    'data' => $this->mergeTwoArraysObjects(),
    'method' => 'GET',


    3089 "Abiu"
    3090 "Cherry"
    id: "3089"
    title: "This is a title"
    body: "This is my body"

    Ok, this is good, but I want to know how to display that data in a website format. Do I need to use twig or I can just do it inside of Controller.php file? How to call that GET to display each of the variables I have?

    Such as,

    Title: This is a title
    More info: This is my body
    Title: This is a title
    More info: This is my body

    And so on...
    I was trying to do this, but it didn't works

    This is part of a function from my Controller.php file
    foreach ($ajax_response as $key1 => $finalValues) {

    if ($key1 == 0) {
    foreach ($finalValues as $disCrop) {
    $content .= "<details>";
    $content .= '<summary>' . $disCrop . '</summary>';


    $content .= "</details>";

    return new JsonResponse([
    '#type' => 'markup',
    '#markup' => $content,

    I'm sorry, I'm new in drupal 8. And, thank you for taking all this time.

  • 2019-04-15 Victor Bocharsky

    Hey Viktor,

    Symfony HttpFoundation Component provides you a few response objects that extend the base Response object. You may read more about them in the docs: . So, regarding to your question, if you want to return *JSON* response - you can use JsonResponse object: . And nope, in this case you even don't need to json_encode() data, the JsonResponse object will do it for you, just pass data as array and that's it:

    return JsonReponse([
    '#type' => 'markup'
    '#markup' => $content,

    But keep in mind that headers will be automatically set to "application/json" with it.


  • 2019-04-12 Viktor

    ok, thanks.

    I would like get back to your code.

    public function roar()
    return new Response('ROOOOOAR!');

    How can you retrieve or showing data on your "path: /the/dino/says" routing. "You are just returning a t('text') Response, but that's it.

    For instance, in a project I have (doesn't apply to your project, because the code is different)

    $content .= "<h1>Hello</h1>";

    return array(
    '#type' => 'markup'
    '#markup' => $content,

    How do you do that "new Response"?

    I have used return new Response::nameofmyobject(); Doesn't work.

    Does the data need to be json before you call it?


  • 2019-04-12 weaverryan

    Hey Viktor!

    That's outside the scope of this tutorial, as we're just covering the new services, routing & dependency injection type of stuff :). But I can point you at one cool feature, which is related to this: parameter upcasting: - this is one way to get access to a Node from your controller, so that you can do something with it.


  • 2019-04-11 Viktor

    How about displaying real data from nodes

  • 2017-01-09 weaverryan

    Awesome :). If you make some progress, I'd love to hear about it!

  • 2017-01-09 Matt Holbrook-Bull

    Cool, I had a little play about over the weekend but ran out of time. I'm going to give it a try, out of pure bloody mindedness if nothing else!

    I'll keep you posted.



  • 2017-01-08 weaverryan

    Yo Matt!

    As far as I can tell... surprisingly... this doesn't seem to be supported, or even too easily added into the core of Drupal! Part of the problem is that, in Symfony, some of the @Route functionality comes from SensioFrameworkExtraBundle - i.e. a *Bundle*... not actually from the core of Symfony, which is shared by Drupal.

    So... it depends on how you define "without too much pain" :). And actually, it would be kind of fun to try out. Here is how (I think) it would work:

    1) Obviously, start by adding some annotations to a controller. Use this class for the use statement:

    2) Create a subscriber on RoutingEvents::ALTER - as described here:

    3) In alterRoutes, instead of altering routes, we'll be adding new ones! How? Basically, you'll create a new instance of this class: - then you'll need to do a little bit of magic to (A) scan for controller classes and (B) get a list of public methods via reflection and then use getMethodAnnotations to read them. What you're looking for is any Route annotations that you added. You can use these to manually create real Route objects (these: and add those to the collection. I realize that I just smashed 5 big steps into one here... but I thought I'd get you started... IF you're even interested.

    Honestly, this would make for a really good mini-screencast! Because it's totally possible! But, you need to understand a bit about how things work to make it happen. Anyways, if you actually want to make this happen and have some questions, let me know!


  • 2017-01-08 Matt Holbrook-Bull

    hi Ryan et all,

    is it possible to annotate routes a' la symfony? I hate routing files and much prefer everything to be together.. as we have a symfony core is there a way of achieving this without too much pain?



  • 2015-12-08 weaverryan

    Hey guys!

    Yes, you can of course create nodes in the admin interface like always. By "custom pages", I mean creating custom paths for custom interactions - not just for reading node content. For example, you could build an entire mini-application inside Drupal using routes that has *nothing* to do with the CMS - e.g. a set of pages where uses can send and receive messages or a set of pages containing a multi-step form for creating an "order" of some sort.


  • 2015-12-07 Jgrubb

    Pretty sure this was just intended to be a really simple demo module. You can still create pages as you always have in the UI.

  • 2015-12-07 WEB BY DOUG

    The concept that custom pages are modules is new to me. I've been developing drupal sites since 4.7 and never heard that before. Is that a new concept in D8?