Drupal's JSON:API module provides a robust, standards-compliant REST API that exposes content entities in a consistent, hypermedia-driven format. For Java applications, this represents a powerful opportunity to leverage Drupal's sophisticated content management capabilities while building enterprise-grade applications with Java's ecosystem. This integration bridges the gap between Drupal's content modeling strengths and Java's performance and scalability.
Understanding Drupal JSON:API Architecture
Drupal JSON:API follows the JSON:API specification, providing:
- Standardized resource objects with consistent structure
- Hypermedia links for navigation and relationships
- Sparse fieldsets for optimized payloads
- Includes for efficient relationship loading
- Filtering, sorting, and pagination out of the box
Core Integration Patterns
1. REST Client Implementation with Spring
Java applications can consume Drupal's JSON:API using standard HTTP clients with proper JSON:API handling.
Base JSON:API Client:
@Service
public class DrupalJsonApiClient {
private final WebClient webClient;
@Value("${drupal.jsonapi.base-url}")
private String baseUrl;
@Value("${drupal.jsonapi.api-key}")
private String apiKey;
public DrupalJsonApiClient() {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.api+json")
.defaultHeader("X-API-Key", apiKey)
.build();
}
public Mono<JsonApiResponse> getCollection(String resourceType, Map<String, String> params) {
return webClient.get()
.uri(buildUri("/jsonapi/{resourceType}", resourceType, params))
.retrieve()
.bodyToMono(JsonApiResponse.class);
}
public Mono<JsonApiResponse> getResource(String resourceType, String uuid, Map<String, String> params) {
return webClient.get()
.uri(buildUri("/jsonapi/{resourceType}/{id}", resourceType, uuid, params))
.retrieve()
.bodyToMono(JsonApiResponse.class);
}
private String buildUri(String path, Object... variables) {
return baseUrl + UriComponentsBuilder.fromPath(path)
.buildAndExpand(variables)
.toUriString();
}
}
2. JSON:API Response Handling
Create specialized services to handle Drupal's JSON:API response structure.
Response Wrapper and Service:
@Data
public class JsonApiResponse {
private List<JsonApiData> data;
private JsonApiLinks links;
private Map<String, Object> meta;
private List<JsonApiData> included;
@Data
public static class JsonApiData {
private String type;
private String id;
private Map<String, Object> attributes;
private Map<String, Object> relationships;
private JsonApiLinks links;
}
@Data
public static class JsonApiLinks {
private String self;
private String related;
private String first;
private String last;
private String next;
private String prev;
}
}
@Service
public class ArticleService {
private final DrupalJsonApiClient apiClient;
public ArticleService(DrupalJsonApiClient apiClient) {
this.apiClient = apiClient;
}
public Flux<Article> getPublishedArticles(int page, int size) {
Map<String, String> params = Map.of(
"page[limit]", String.valueOf(size),
"page[offset]", String.valueOf(page * size),
"filter[status][value]", "1",
"sort", "-created",
"fields[node--article]", "title,body,created,field_image"
);
return apiClient.getCollection("node--article", params)
.flatMapMany(response -> Flux.fromIterable(response.getData()))
.map(this::mapToArticle);
}
public Mono<Article> getArticleWithRelationships(String uuid) {
Map<String, String> params = Map.of(
"include", "field_image,field_tags",
"fields[file--file]", "uri,filename",
"fields[taxonomy_term--tags]", "name"
);
return apiClient.getResource("node--article", uuid, params)
.map(response -> {
Article article = mapToArticle(response.getData().get(0));
mapIncludedRelationships(article, response.getIncluded());
return article;
});
}
private Article mapToArticle(JsonApiResponse.JsonApiData data) {
Article article = new Article();
article.setUuid(data.getId());
article.setTitle((String) data.getAttributes().get("title"));
article.setBody((String) data.getAttributes().get("body"));
article.setCreated(LocalDateTime.parse((String) data.getAttributes().get("created")));
return article;
}
}
3. Advanced Query Building
Handle complex JSON:API queries with a fluent builder pattern.
Query Builder Implementation:
@Component
public class DrupalQueryBuilder {
public String buildArticleQuery(ArticleQuery query) {
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/jsonapi/node/article");
// Fields filtering
if (!query.getFields().isEmpty()) {
builder.queryParam("fields[node--article]",
String.join(",", query.getFields()));
}
// Includes
if (!query.getIncludes().isEmpty()) {
builder.queryParam("include",
String.join(",", query.getIncludes()));
}
// Filters
query.getFilters().forEach((key, value) -> {
builder.queryParam("filter[" + key + "]", value);
});
// Sorting
if (query.getSort() != null) {
builder.queryParam("sort", query.getSort());
}
// Pagination
builder.queryParam("page[limit]", query.getPageSize());
builder.queryParam("page[offset]", query.getOffset());
return builder.build().toUriString();
}
@Data
public static class ArticleQuery {
private List<String> fields = Arrays.asList("title", "body", "created");
private List<String> includes = new ArrayList<>();
private Map<String, String> filters = new HashMap<>();
private String sort = "-created";
private int pageSize = 10;
private int page = 0;
public int getOffset() {
return page * pageSize;
}
public ArticleQuery addFilter(String field, String value) {
filters.put(field + "[value]", value);
return this;
}
public ArticleQuery addCondition(String condition, String value) {
filters.put(condition, value);
return this;
}
}
}
4. Caching and Performance Optimization
Implement sophisticated caching for Drupal content.
Cache Configuration:
@Service
@CacheConfig(cacheNames = "drupalContent")
public class CachedDrupalService {
private final ArticleService articleService;
public CachedDrupalService(ArticleService articleService) {
this.articleService = articleService;
}
@Cacheable(key = "'article_' + #uuid")
public Article getArticle(String uuid) {
return articleService.getArticleWithRelationships(uuid).block();
}
@Cacheable(key = "'articles_page_' + #page + '_size_' + #size")
public List<Article> getArticles(int page, int size) {
return articleService.getPublishedArticles(page, size)
.collectList()
.block();
}
@CacheEvict(key = "'article_' + #uuid")
public void evictArticle(String uuid) {
// Cache eviction handled by annotation
}
@Scheduled(fixedRate = 300000) // 5 minutes
@CacheEvict(allEntries = true)
public void evictAllArticles() {
// Periodic cache refresh
}
}
5. Webhook Integration for Real-time Updates
Handle Drupal content updates in real-time.
Webhook Controller:
@RestController
@RequestMapping("/webhooks/drupal")
public class DrupalWebhookController {
private final CachedDrupalService drupalService;
public DrupalWebhookController(CachedDrupalService drupalService) {
this.drupalService = drupalService;
}
@PostMapping("/content-update")
public ResponseEntity<String> handleContentUpdate(@RequestBody DrupalWebhookPayload payload) {
String eventType = payload.getEvent();
String entityType = payload.getEntityType();
String entityUuid = payload.getEntityUuid();
switch (eventType) {
case "entity.insert":
case "entity.update":
handleEntityUpdate(entityType, entityUuid);
break;
case "entity.delete":
handleEntityDelete(entityType, entityUuid);
break;
}
return ResponseEntity.ok("Webhook processed");
}
private void handleEntityUpdate(String entityType, String entityUuid) {
if ("node--article".equals(entityType)) {
drupalService.evictArticle(entityUuid);
// Trigger reindexing or other business logic
searchService.indexArticle(entityUuid);
}
}
private void handleEntityDelete(String entityType, String entityUuid) {
if ("node--article".equals(entityType)) {
drupalService.evictArticle(entityUuid);
searchService.removeArticle(entityUuid);
}
}
}
Error Handling and Resilience
Comprehensive Error Handling:
@ControllerAdvice
public class DrupalIntegrationExceptionHandler {
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<ErrorResponse> handleDrupalApiError(WebClientResponseException ex) {
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("Content not found in Drupal"));
}
if (ex.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(new ErrorResponse("Drupal API rate limit exceeded"));
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Drupal integration error: " + ex.getMessage()));
}
}
@Component
public class DrupalCircuitBreaker {
private final CircuitBreaker circuitBreaker;
public DrupalCircuitBreaker() {
this.circuitBreaker = CircuitBreaker.ofDefaults("drupalApi");
}
public <T> Mono<T> callWithResilience(Mono<T> drupalCall) {
return Mono.delegate(() -> drupalCall)
.transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
.onErrorResume(throwable -> {
log.warn("Drupal API call failed, using fallback", throwable);
return getFallbackContent();
});
}
}
Best Practices for Java-Drupal JSON:API Integration
- API Key Management: Secure Drupal JSON:API with API keys and store them securely
- Rate Limiting: Implement client-side rate limiting to respect Drupal API limits
- Connection Pooling: Configure WebClient with proper connection pooling
- Timeout Configuration: Set appropriate timeouts for Drupal API calls
- Content Type Handling: Always use
application/vnd.api+jsoncontent type - Error Logging: Implement comprehensive logging for debugging integration issues
- Health Checks: Monitor Drupal API availability with health checks
Conclusion: Enterprise-Grade Content Integration
Integrating Drupal JSON:API with Java applications creates a powerful synergy that leverages Drupal's superior content modeling and management capabilities with Java's enterprise strengths. This approach enables organizations to use Drupal as a sophisticated content hub while building high-performance, scalable applications in Java.
By treating Drupal as your content engine and Java as your application engine, you achieve the perfect balance between editorial flexibility and technical robustness—proving that decoupled architectures can deliver both content excellence and technical performance.