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.
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
@RestResourceannotation, or add apost()method to the same class? (Answer: apost()method on the same class — one resource plugin can implementget(),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@RestResourceannotation 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
ResourceResponsewithaddCacheableDependency(), 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.