Drupal is a registered trademark of Dries Buytaert
drupal 11.3.7 Update released for Drupal core (11.3.7)! drupal 11.2.11 Update released for Drupal core (11.2.11)! drupal 10.6.7 Update released for Drupal core (10.6.7)! drupal 10.5.9 Update released for Drupal core (10.5.9)! cms 2.1.1 Update released for Drupal core (2.1.1)! drupal 11.3.6 Update released for Drupal core (11.3.6)! drupal 10.6.6 Update released for Drupal core (10.6.6)! cms 2.1.0 Update released for Drupal core (2.1.0)! bootstrap 8.x-3.40 Minor update available for theme bootstrap (8.x-3.40). menu_link_attributes 8.x-1.7 Minor update available for module menu_link_attributes (8.x-1.7). eca 3.1.1 Minor update available for module eca (3.1.1). layout_paragraphs 2.1.3 Minor update available for module layout_paragraphs (2.1.3). ai 1.3.3 Minor update available for module ai (1.3.3). ai 1.2.14 Minor update available for module ai (1.2.14). node_revision_delete 2.0.3 Minor update available for module node_revision_delete (2.0.3). moderated_content_bulk_publish 2.0.52 Minor update available for module moderated_content_bulk_publish (2.0.52). klaro 3.0.10 Minor update available for module klaro (3.0.10). klaro 3.0.9 Minor update available for module klaro (3.0.9). layout_paragraphs 2.1.2 Minor update available for module layout_paragraphs (2.1.2). geofield_map 11.1.8 Minor update available for module geofield_map (11.1.8).

api_toolkit

14 sites Security covered
View on drupal.org

The JSON:API core module is a good choice for easy to set up, standardised API
endpoints for your Drupal entities. However, sometimes it's just a better idea
to create a custom, tailored API endpoint. API Toolkit aims to help you do just
that.

This module is a developer tool: it doesn't do anything in itself, but it can
help you creating consistency and reducing boilerplate when creating custom API
endpoints.

Request classes

Create domain-specific request classes with automatically filled properties
that can be injected in your controller methods through arguments. Symfony
validation rules can be defined on properties through annotations.

The module will look for values to assign to the properties in the following
places:

  • POST requests with a content type of application/x-www-form-urlencoded,
    multipart/form-data or application/json
  • GET requests with query parameters
  • Route parameters and other request attributes

Properties type hints are also used for validation. For example, when typing
a property as string, the property will be required and only strings will be
allowed. When typing a property as ?int, the property will be optional and
only numbers will be allowed.

Property default values also influence validation. If a property has a
default value it will be considered optional. If the property does not have a
default value and is not nullable, it will be required. Objects (associative
arrays in PHP) can be validated using the Collection constraint.
Don't forget that properties are considered nullable by default, so if a nested
property is required you should add a NotBlank or NotNull constraint.

Enums were introduced in PHP 8.1 and are also supported in this module.
When type hinting a property as a backed enum, only the values of its cases
will be allowed. If an invalid value is passed, the validation error will list
the allowed values. The string value will also automatically be converted to an
instance of the enum.

Cacheability metadata like query parameter cache contexts are automatically
added to the request object. Make sure to add the request object as cacheable
dependency to the response, at least if it's cacheable.

Example

namespace Drupal\my_module\Request;

use Drupal\api_toolkit\Plugin\Validation\Constraint\EntityExists;
use Drupal\api_toolkit\Request\ApiRequestBase;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Request class for creating an example page.
 *
 * @see \Drupal\ my_module\Controller\ExamplePageApiController::post()
 */
class CreateExamplePageRequest extends ApiRequestBase {

  /**
   * @Assert\Length( max = 255 )
   */
  public string $title;

}
namespace Drupal\my_module\Controller;

/**
 * Controller for CRUD API endpoints for example pages.
 */
class ExamplePageApiController implements ContainerInjectionInterface {

  protected EntityTypeManagerInterface $entityTypeManager;
  protected ValidatorInterface $validator;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = new static();
    $instance->entityTypeManager = $container->get('entity_type.manager');
    $instance->validator = $container->get('api_toolkit.validator');

    return $instance;
  }

  /**
   * Create a new example page.
   */
  public function post(CreateExamplePageRequest $request): Response {
    $violations = $this->validator->validate($request);

    if ($violations->count() > 0) {
      throw ApiValidationException::create($violations);
    }

    $examplePage = ExamplePage::create();
    $examplePage->setTitle($request->title);
    $examplePage->save();

    return Response::create(status: Response::HTTP_NO_CONTENT);
  }

}

Standardised responses

This module provides tools to create consistent JSON responses. Some examples:

Drupal\api_toolkit\Response\(Cacheable)JsonResponse

When using these classes, all data is nested under a data key.

# GET /api/example-pages/all
{
    "data": [
        {
            "id": "474785",
            "title": "An example page",
            "created": "1664635685",
            "author": {
                "id": "0",
                "displayName": ""
            },
            "link": {
                "url": "https:\/\/example.com",
                "title": null
            },
            "similarPages": []
        }
    ]
}

Drupal\api_toolkit\Response\(Cacheable)PagedJsonResponse

When using these classes, all data is nested under a data key, information
about the pager is added under the pagination key and previous/next links are
added under the links key.

# GET /api/example-pages/paged?page=1
{
    "pagination": {
        "currentPage": 1,
        "totalPages": 2,
        "totalItems": 2,
        "limit": 1
    },
    "data": [
        {
            "id": "474786",
            "title": "Another example page",
            "created": "1664635685",
            "author": {
                "id": "0",
                "displayName": ""
            },
            "link": {
                "url": "https:\/\/another.example.com",
                "title": null
            },
            "similarPages": []
        }
    ],
    "links": {
        "prev": "\/api\/example-pages\/paged?page=0"
    }
}

Drupal\api_toolkit\Response\ApiErrorJsonResponse

When throwing instances of Drupal\api_toolkit\Exception\ApiValidationException
in controllers or in case of automatic type validation errors using request
classes, standardised error responses are automatically built and returned. This
behaviour is only enabled for the route formats specified in the route_formats
option of the api_toolkit.settings config.

# POST /api/example-pages
# Accept: application/json
# Content-Type: application/json
# 
# {
#     "title": ""
# }

{
    "errors": [
        {
            "path": "title",
            "message": "This value should not be blank."
        }
    ]
}

Custom validators

A couple custom validators are provided to be used with the request classes:

  • @EntityExists: Validates whether an entity with a certain field value (ID,
    UUID, etc.) exists.
  • @Langcode: Validates whether a certain value is the langcode of an installed
    language.
  • MigrationSourceExists: Validates whether a certain source ID exists in the
    mapping of a migration.

Known issues

The controller result claims to be providing relevant cache metadata, but leaked metadata was detected

Often when creating custom API endpoints you'll get the following error message:

The controller result claims to be providing relevant cache metadata, but
leaked metadata was detected. Please ensure you are not rendering content too 
early. Returned object class: Drupal\api_toolkit\Response\CacheableJsonResponse.

This happens every time you render something without collecting the
cacheability metadata. In practice, most of the time this error will appear
after generating some URL. There has been lots of discussion about this approach
in #2638686 and the
issue and possible solutions have been clearly explained in this Lullabot article.
The easiest solution would be to add the patch from the core issue to your
project, but make sure to only do this if you understand the implications.

Activity

Total releases
4
First release
Mar 2025
Latest release
3 months ago
Release cadence
99 days
Stability
100% stable

Release Timeline

Releases

Version Type Release date
1.5.1 Stable Dec 29, 2025
1.5.0 Stable Dec 29, 2025
1.4.0 Stable Oct 27, 2025
1.3.0 Stable Mar 7, 2025