How to create a custom endpoint using Drupal JSON:API

Auteur(s) de l'article

If you’re looking for a way to provide a custom collections like jsonapi/articles/featured or contextual data like a jsonapi/me. You’re in the good place.
In this article, I will not explain how to create a custom REST Resources.
If you’re not familiar with JSON:API, I recommend you watch the following short video. It will provide valuable context for the remainder of this blog post:
Here I will try to expose you a step-by-step guide which explains why and how you can create a custom JSON:API Resource that leverage JSON:API Core feature.

JSON:API in short

To be franc - I don't want to read the story. Bring me to the code.

The JSON:API module is becoming wildly popular as an out-of-the-box way to provide an API server.
Why? Because it implements the {json:api} specification.

It’s still a RESTful interface, but the specification just helps bring an open standard for how data should be represented and requests should be constructed.

oopsie…
oopsie…

JSON:API Resource vs REST Resources ?

The principal interest of codding a JSON:API Resource instead of a classic REST Resource is to leverage the same standardized {json:api} output in your custom resource than the output of Drupal JSON:API out-of-the-box collections endpoints.
Believe me, frontenders will prefer dealing with 1 specification than having to transform and specify customization for few unstandardized output.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Martin Fowler

Another advantage of extending the JSON:API with your own collection is the Core ability that will be available out-of-the-box:
  • Sorting
  • Filtering
  • Paginate
  • Selection of fields
  • Inclusion of relation

    Deep Dive in Code

    First, you will need to install a contrib module drupal/jsonapi_resources.
    composer require drupal/jsonapi_resources
    Don’t worry, the module is trustable, tested and stable. Plus it has been made by mglaman, one of the core-contributor of JSON:API for Drupal.

    Routing

    Before starting your custom Query Builder, you will need to define you’re own new route.
    Let's defines a route for a collection containing featured articles nodes.
    # Defines a route for a collection containing featured articles nodes.
    my_custom_resource.jsonapi_resources.featured_articles:
      # %jsonapi% is a placeholder for the JSON:API base path, which can be
      # configured in a site's services.yml file.
      path: '/%jsonapi%/articles/featured'
      defaults:
        # Every JSON:API resource route must declare a _jsonapi_resource. The
        # value can either be a class or a service ID. Unlike the _controller
        # route default, it is not possible to declare a method name to be called.
        _jsonapi_resource: Drupal\my_custom_resource\Resource\FeaturedArticlesResource
        _jsonapi_resource_types: ['node--article']
      requirements:
        _permission: 'access content'
    In case you need adding options (eg. a node or user), the same features as for common routes/controllers are available.
    # Defines a route for a collection containing related articles of given node.
      path: '/%jsonapi%/articles/{page}/related'
      defaults:
        _jsonapi_resource: Drupal\my_custom_resource\Resource\RelatedArticlesOfPageResource
        _jsonapi_resource_types: ['node--article']
      requirements:
        _permission: 'access content'
      options:
        parameters:
          agent:
            type: entity:node
    See the Structure of routes for more informations.

    The Resource

    Processes a request for a collection containing featured articles Nodes.
    <?php
    
    namespace Drupal\my_custom_resource\Resource;
    
    use Drupal\Core\Cache\CacheableMetadata;
    use Drupal\jsonapi\ResourceResponse;
    use Drupal\jsonapi_resources\Resource\EntityQueryResourceBase;
    use Drupal\node\NodeInterface;
    use Symfony\Component\HttpFoundation\Request;
    
    /**
     * Processes a request for a collection containing featured articles nodes.
     */
    class FeaturedArticlesResource extends EntityQueryResourceBase {
    
      /**
       * Process the resource request.
       *
       * @param \Symfony\Component\HttpFoundation\Request $request
       *   The request.
       *
       * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
       * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
       *
       * @return \Drupal\jsonapi\ResourceResponse
       *   The response.
       */
      public function process(Request $request): ResourceResponse {
        $cacheability = new CacheableMetadata();
    
        /** @var \Drupal\Core\Entity\ContentEntityType $entity_type */
        $entity_type = $this->entityTypeManager->getDefinition('node');
        /** @var string $bundle_field */
        $bundle_field = $entity_type->getKey('bundle');
        /** @var string $status_field */
        $status_field = $entity_type->getKey('status');
    
        $entity_query = $this->getEntityQuery('node')
          ->condition($bundle_field, 'article')
          ->condition('promote', TRUE)
          ->condition($status_field, NodeInterface::PUBLISHED);
    
        $cacheability->addCacheContexts(['url']);
    
        $paginator = $this->getPaginatorForRequest($request);
        $paginator->applyToQuery($entity_query, $cacheability);
    
        $data = $this->loadResourceObjectDataFromEntityQuery($entity_query, $cacheability);
    
        $pagination_links = $paginator->getPaginationLinks($entity_query, $cacheability, TRUE);
    
        /** @var \Drupal\jsonapi\CacheableResourceResponse $response */
        $response = $this->createJsonapiResponse($data, $request, 200, [], $pagination_links);
        $response->addCacheableDependency($cacheability);
    
        return $response;
      }
    
    }
    Well done you did it !

    Shia LaBeouf

    That’s pretty much all the hocus-pocus that you need to have a custom JSON:API Resource that leverage JSON:API Core feature.
    Imagination Spongebob

    Digging Deeper

    In case you need to upcast the URL parameter by UUID, I suggest you to use the converter paramconverter.jsonapi.entity_uuid .
    # Defines a route for a collection containing related articles of given node.
      path: '/%jsonapi%/articles/{page}/related'
      defaults:
        _jsonapi_resource: Drupal\my_custom_resource\Resource\RelatedArticlesOfPageResource
        _jsonapi_resource_types: ['node--article']
      requirements:
        _permission: 'access content'
      options:
        parameters:
          agent:
            type: entity:node
            # Upcast entity parameters by UUID.
            converter: 'paramconverter.jsonapi.entity_uuid'

    Sources

    For the most curious of you, here are some sources of additional information that inspired the creation of this article.
    Gabe Sullice (20 September, 2019) A New Era for Drupal’s JSON:API.
    Seen on: https://sullice.com/.../a-new-era-for-drupals-jsonapi
    Ivo Kovačević (03 Jully, 2019). How to create custom JSON-API resource drupal 8.
    Seen on https://stackoverflow.com/.../how-to-create-custom-...
    JSON:API Resources Documentation.
    Seen on https://drupal.org/project/commerce/issues/2938731
    Dries Buytaert (11 February, 2019). Headless CMS: REST vs JSON:API vs GraphQL.
    Seen on https://dri.es/headless-cms-rest-vs-jsonapi-vs-graphql
    JSON:API Search API Documentation.
    Seen on: https://drupal.org/project/jsonapi_search_api

    Have a Drupal project? We can certainly help! Visit our page to learn more.