Defining a Custom REST Resource Plugin in Drupalfor Drupal 8 , 9 , 10 , and 11

Last updated :  

A REST resource plugin is Drupal's way of saying "here's a URL, and here's exactly what happens when something GETs, POSTs, or DELETEs it" — no controller, no routing.yml by hand, just a single annotated PHP class that Drupal's Plugin system discovers and wires up to a real HTTP endpoint automatically. If you've ever needed a mobile app, a JavaScript frontend, or another server to talk to your Drupal site over JSON, this is the building block that makes it possible.

What you'll learn in this lesson

  • What a REST resource plugin actually is, and how it differs from a normal controller
  • The exact folder structure and annotation Drupal needs to discover one
  • How to implement a working get() method that returns real JSON
  • Why cache metadata matters even on a "simple" API response

Enabling the REST and Serialization modules

Two core modules do the heavy lifting here: REST (the plugin system and HTTP method dispatch) and Serialization (turning PHP arrays into JSON or XML). Enable both before writing any code:

drush en rest serialization -y

The plugin's folder and annotation

Like every plugin type in Drupal, a REST resource lives at a fixed, PSR-4-discoverable path — src/Plugin/rest/resource/ inside your module:

mkdir -p src/Plugin/rest/resource

Inside it, a single PHP class with an @RestResource annotation is everything Drupal needs to register a new API endpoint — no routing.yml, no controller registration:

<?php

namespace Drupal\my_rest_module\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a My Custom Resource.
 *
 * @RestResource(
 *   id = "my_custom_resource",
 *   label = @Translation("My Custom Resource"),
 *   uri_paths = {
 *     "canonical" = "/api/v1/custom-resource"
 *   }
 * )
 */
class MyCustomResource extends ResourceBase {

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('my_rest_module')
    );
  }

  /**
   * Responds to GET requests.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response, serialized to JSON (or XML) automatically.
   */
  public function get() {
    $data = ['message' => 'Hello, welcome to our custom REST API!'];

    $response = new ResourceResponse($data);
    $response->addCacheableDependency($data);
    return $response;
  }

}

A few details worth noticing: the class extends ResourceBase, not a plain PHP class — that base class supplies the logger and constructor plumbing every resource needs. The create() method is the same dependency-injection factory pattern you've seen on forms and controllers throughout this course, just pulling in serializer.formats and a logger instead of a repository or entity manager. And get() takes no arguments here because this endpoint doesn't need one — a resource that responds to /api/v1/custom-resource/{id} would receive that {id} as a method parameter instead.

Don't skip addCacheableDependency(). Returning a ResourceResponse without any cache metadata means Drupal has no way to know when this response should be considered stale — on a site with page caching enabled, that can mean stale API data getting served to every client. Even a static array like this one should declare its caching rules explicitly rather than relying on defaults.

Enabling and testing your resource

Enable your module, then configure the resource — annotated REST resources exist as plugins but still need their specific format/authentication combination turned on, either via drush config-import of a REST config entity or through Configuration → REST in the admin UI:

drush en my_rest_module -y

Once enabled, a GET request to /api/v1/custom-resource (with an Accept: application/json header) returns your JSON payload — no HTML wrapper, no theme, just the serialized data.

Quick check: if you wanted this same resource to also accept POST requests, would you add a second @RestResource annotation, or add a post() method to the same class? (Answer: a post() method on the same class — one resource plugin can implement get(), post(), patch(), delete(), and so on, each corresponding to the HTTP method Drupal's REST module dispatches to automatically.)

Key takeaways

  • A REST resource plugin is a single annotated class in src/Plugin/rest/resource/ — the @RestResource annotation is what makes it discoverable, with no routing.yml required.
  • Extend Drupal\rest\Plugin\ResourceBase, not a plain class — it supplies the logger and constructor plumbing every resource needs.
  • Implement one method per HTTP verb you want to support: get(), post(), patch(), delete() — all on the same plugin class.
  • Always attach cache metadata to a ResourceResponse with addCacheableDependency(), even for data that looks "static."
  • Enabling the REST module isn't enough by itself — the specific resource also needs its format/auth combination enabled via config or the admin UI before it responds.

Coming up next

A resource that always returns the same hardcoded message isn't very useful yet. In the next lesson, we'll look at configuring REST endpoints properly — enabling specific formats and authentication providers per resource, so your API can actually be locked down and consumed safely.