api_toolkit
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-dataorapplication/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.