Java is a cross-platform, object-oriented language used for building scalable backends

Java is a general-purpose, object-oriented programming language that runs on the Java Virtual Machine (JVM). It's a mainstay in enterprise backend development, Android applications, and large-scale distributed systems. Current releases highlighted here are Java 25 and Java 21, both of which include java.net.http.HttpClient, a built-in HTTP client standardized in Java 11 that supports synchronous and asynchronous requests, HTTP/2, and CompletableFuture-based async patterns.
Java's ecosystem includes mature HTTP client libraries like OkHttp, Apache HttpClient, and Spring's WebClient, along with JSON processing libraries like Jackson and Gson. This makes it well-suited for consuming REST APIs from external services, including headless CMS platforms like Strapi.
Java handles business logic, enterprise integrations, and existing services. Strapi handles content modeling and delivery. Together, they let Java teams consume managed content without building a custom CMS from scratch.
/graphql endpoint. Java clients start consuming content immediately, with no route definitions or controller boilerplate on the CMS side.data.attributes wrapper from v4, directly reducing the number of intermediate wrapper classes in Java POJO hierarchies./api/upload endpoints for programmatic file management. Java applications upload, retrieve, and associate media assets without a separate Digital Asset Manager.Before writing any Java code, you need a running Strapi v5 instance with content to consume.
Strapi side:
Java side:
HttpClient)2.20.1 for JSON processingScaffold Strapi if you don't have an instance running:
npx create-strapi@latest my-strapi-backendAfter setup, create an Article content type in the Content-Type Builder with fields like title (Text), slug (Text), and content (Rich Text). Add a few entries and publish them: entries in draft state are not returned by the REST API by default. Then grant find and findOne permissions under Settings → Users & Permissions → Roles → Public for your content type.
Verify the endpoint works before touching Java:
curl http://localhost:1337/api/articlesAdd Jackson Databind to your pom.xml. The built-in java.net.http.HttpClient requires no Maven dependency since it ships with Java 11+.
<dependencies>
<!-- Jackson Databind for JSON serialization/deserialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.1</version>
</dependency>
</dependencies>Create a StrapiClient class with shared constants and a reusable HttpClient instance. The HttpClient is immutable and thread-safe; create it once and reuse it across all requests.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
public class StrapiClient {
private static final String BASE_URL = "http://localhost:1337";
private static final HttpClient httpClient = HttpClient.newHttpClient();
private static final ObjectMapper objectMapper = new ObjectMapper();
}Strapi v5 supports multiple authentication methods, including JSON Web Token (JWT) tokens for user sessions and API Tokens for service-to-service calls.
For Java backend services, API Tokens are the recommended approach. Generate one in the Admin Panel under Settings → API Tokens. Three scope levels are available: read-only, full-access, and custom.
Pass the token on every request:
Authorization: bearer <your-api-token>If your use case requires user authentication (for example, a Java backend serving a login flow), use JWT authentication via POST /api/auth/local:
/**
* Authenticates with Strapi and returns a JWT token.
* Use API Tokens instead for service-to-service calls.
*/
public static String login(String identifier, String password) throws Exception {
String loginBody = objectMapper.writeValueAsString(
Map.of("identifier", identifier, "password", password)
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/auth/local"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(loginBody))
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException(
"Login failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
JsonNode root = objectMapper.readTree(response.body());
return root.get("jwt").asText();
}For a read-only Java service consuming public content, a read-only token follows least-privilege principles and reduces risk if the token is accidentally exposed in logs.
This is where Strapi v5's flattened response format matters. Fields sit directly on data.fieldName with no data.attributes wrapper. The documentId is a string, not a numeric ID:
{
"data": [
{
"id": 1,
"documentId": "znrlzntu9ei5onjvwfaalu2v",
"title": "Test Article",
"slug": "test-article",
"createdAt": "2024-03-06T13:42:05.098Z",
"publishedAt": "2024-03-06T13:42:05.103Z",
"locale": "en"
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 }
}
}Fetch all entries in a collection:
/**
* GET /api/restaurants — fetches all entries.
* Response uses v5 flattened format: data[].fieldName (no attributes wrapper).
*/
public static JsonNode getRestaurants(String token) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/restaurants"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException(
"GET failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode data = root.get("data");
for (JsonNode entry : data) {
System.out.println(
"id: " + entry.get("id").asInt()
+ " | documentId: " + entry.get("documentId").asText()
+ " | Name: " + entry.get("Name").asText()
);
}
return root;
}Fetch a single entry by documentId:
/**
* GET /api/restaurants/:documentId
* The path parameter in v5 is the documentId string, not a numeric id.
*/
public static JsonNode getRestaurantByDocumentId(String documentId, String token)
throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/restaurants/" + documentId))
.header("Authorization", "Bearer " + token)
.GET()
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() == 404) {
throw new RuntimeException("Not found: documentId=" + documentId);
}
if (response.statusCode() != 200) {
throw new RuntimeException(
"GET failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
return objectMapper.readTree(response.body()).get("data");
}Query parameters give you fine-grained control over results. Strapi v5 uses LHS bracket syntax for filtering, sorting, pagination, and population:
GET /api/restaurants?filters[stars][$gte]=3&filters[open][$eq]=true
GET /api/articles?sort=publishedAt:desc&pagination[page]=1&pagination[pageSize]=25
GET /api/articles?fields[0]=title&fields[1]=slug&populate[category]=true
GET /api/articles?status=publishedOne thing that trips up nearly every developer on first contact: Strapi does not populate relations by default. You need to explicitly request them with the populate parameter. Forget this, and your Java client receives empty fields for all relational data.
POST requests should follow the request body format documented for the specific Strapi API you are using. Strapi v5 validates input by default, so payloads that don't match the content-type schema return HTTP 400.
/**
* POST /api/restaurants — creates a new entry.
* The request body must include a "data" wrapper.
* Returns the new entry's documentId.
*/
public static String createRestaurant(String name, String description, String token)
throws Exception {
String requestBody = objectMapper.writeValueAsString(
Map.of(
"data", Map.of(
"Name", name,
"Description", description
)
)
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/restaurants"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new RuntimeException(
"POST failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
JsonNode created = objectMapper.readTree(response.body()).get("data");
String newDocumentId = created.get("documentId").asText();
System.out.println("Created entry. documentId: " + newDocumentId);
return newDocumentId;
}PUT requests target a specific documentId in the URL path. Include only the fields you're updating.
/**
* PUT /api/restaurants/:documentId — updates an existing entry.
* Path param is the documentId string. Body requires "data" wrapper.
*/
public static void updateRestaurant(String documentId, String newName, String token)
throws Exception {
String requestBody = objectMapper.writeValueAsString(
Map.of("data", Map.of("Name", newName))
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/restaurants/" + documentId))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException(
"PUT failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
System.out.println("Updated documentId=" + documentId + " → HTTP " + response.statusCode());
}DELETE also targets :documentId. In Strapi v5, the REST DELETE endpoint for a collection entry typically returns HTTP 204 (No Content) on success, not the deleted entry data.
/**
* DELETE /api/restaurants/:documentId — removes an entry.
* Strapi v5 returns HTTP 204 on success and does not return any data in the response body.
*/
public static void deleteRestaurant(String documentId, String token) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/restaurants/" + documentId))
.header("Authorization", "Bearer " + token)
.DELETE()
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString());
if (response.statusCode() != 204) {
throw new RuntimeException(
"DELETE failed: HTTP " + response.statusCode()
+ " — " + response.body()
);
}
System.out.println("Deleted documentId=" + documentId + " → HTTP " + response.statusCode());
}Tie all the operations together in a main() method to verify the integration end to end:
public static void main(String[] args) throws Exception {
// Authenticate — use API Token for production, JWT for testing
String token = StrapiClient.login("admin@example.com", "yourpassword");
System.out.println("Authenticated. JWT acquired.");
// GET all entries
getRestaurants(token);
// POST — create entry, capture documentId
String docId = createRestaurant("Java Café", "A café for Java developers", token);
// GET single entry by documentId
JsonNode entry = getRestaurantByDocumentId(docId, token);
System.out.println("Fetched:\n" + entry.toPrettyString());
// PUT — update using documentId
updateRestaurant(docId, "Java Café (Updated)", token);
// DELETE — remove using documentId
deleteRestaurant(docId, token);
}This project demonstrates a practical architecture where Strapi v5 manages editorial content and a Spring Boot application fetches and serves it. Editors work in Strapi's Admin Panel; the Java backend applies business logic, caching, and exposes clean API endpoints for the frontend.
Data flow:
[Editor] → Strapi Admin Panel → Strapi v5 (PostgreSQL)
↓ REST: GET /api/articles
Spring Boot BlogService.java
↓ Java POJOs
BlogController.java → /blog endpoints
↓
Thymeleaf / React / AngularStrapi Content Type: Article
| Field | Type | Notes |
|---|---|---|
title | Text | Required |
slug | Text | Required, Unique |
content | Rich Text (Blocks) | Main body |
category | Enumeration | tutorial, news |
publishedAt | Auto-managed | Draft & Publish |
Define this content type in the Content-Type Builder, add several entries, and publish them before proceeding.
Project structure:
blog-reader/
├── src/main/java/com/example/blogreader/
│ ├── BlogReaderApplication.java
│ ├── config/
│ │ └── RestTemplateConfig.java
│ ├── model/
│ │ ├── Article.java
│ │ ├── StrapiResponse.java
│ │ └── Meta.java
│ ├── service/
│ │ └── BlogService.java
│ └── controller/
│ └── BlogController.java
└── src/main/resources/
└── application.propertiesJava data models (v5-compatible):
The Article class maps fields directly at the top level with no attributes nesting. The documentId is a String, not an integer. Including locale reflects Strapi v5's core i18n feature, which adds a locale field to API responses, but the official docs do not state that it must be modeled to avoid deserialization issues.
import java.util.List;
// Generic envelope for Strapi v5 list responses
public class StrapiResponse<T> {
private List<T> data;
private Meta meta;
public List<T> getData() { return data; }
public void setData(List<T> data) { this.data = data; }
public Meta getMeta() { return meta; }
public void setMeta(Meta meta) { this.meta = meta; }
}
// Pagination metadata
public class Meta {
private Pagination pagination;
public Pagination getPagination() { return pagination; }
public void setPagination(Pagination pagination) { this.pagination = pagination; }
}
public class Pagination {
private int page;
private int pageSize;
private int pageCount;
private int total;
// getters and setters
}// Article.java — fields flattened at top level per v5 format
public class Article {
private String documentId; // String, not int — v5 requirement
private String title;
private String slug;
private String content;
private String category;
private String publishedAt;
private String locale;
public String getDocumentId() { return documentId; }
public void setDocumentId(String documentId) { this.documentId = documentId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getPublishedAt() { return publishedAt; }
public void setPublishedAt(String publishedAt) { this.publishedAt = publishedAt; }
public String getLocale() { return locale; }
public void setLocale(String locale) { this.locale = locale; }
}application.properties:
strapi.base-url=http://localhost:1337
strapi.api-token=your-read-only-api-token-hereRestTemplateConfig.java:
// RestTemplateConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}BlogService.java:
The service layer handles all HTTP communication with Strapi. Note the ?status=published parameter, which keeps the query focused on published items.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.List;
@Service
public class BlogService {
private final RestTemplate restTemplate;
@Value("${strapi.base-url}")
private String strapiBaseUrl;
@Value("${strapi.api-token}")
private String apiToken;
public BlogService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<Article> getPublishedArticles() {
String url = strapiBaseUrl
+ "/api/articles?sort=publishedAt:desc&status=published";
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
var response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
new ParameterizedTypeReference<StrapiResponse<Article>>() {}
);
return response.getBody() != null
? response.getBody().getData()
: Collections.emptyList();
}
public Article getArticleByDocumentId(String documentId) {
String url = strapiBaseUrl + "/api/articles/" + documentId;
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
var response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
new ParameterizedTypeReference<StrapiSingleResponse<Article>>() {}
);
return response.getBody() != null
? response.getBody().getData()
: null;
}
public List<Article> getArticlesByCategory(String category) {
String url = strapiBaseUrl
+ "/api/articles?filters[category][$eq]=" + category
+ "&sort=publishedAt:desc&status=published";
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
var response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
new ParameterizedTypeReference<StrapiResponse<Article>>() {}
);
return response.getBody() != null
? response.getBody().getData()
: Collections.emptyList();
}
}You also need a single-item response wrapper:
// StrapiSingleResponse.java — for single-entry endpoints
public class StrapiSingleResponse<T> {
private T data;
private Meta meta;
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public Meta getMeta() { return meta; }
public void setMeta(Meta meta) { this.meta = meta; }
}BlogController.java:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class BlogController {
private final BlogService blogService;
public BlogController(BlogService blogService) {
this.blogService = blogService;
}
@GetMapping("/blog")
public List<Article> listArticles() {
return blogService.getPublishedArticles();
}
@GetMapping("/blog/{documentId}")
public Article getArticle(@PathVariable String documentId) {
return blogService.getArticleByDocumentId(documentId);
}
@GetMapping("/blog/category/{category}")
public List<Article> listByCategory(@PathVariable String category) {
return blogService.getArticlesByCategory(category);
}
}Architecture note: Consider adding a dto/ProductDTO.java layer that strips CMS metadata (documentId, locale, publishedAt) from the public response. This decouples your frontend API contract from Strapi's response shape. If Strapi's structure changes in a future version, the impact doesn't cascade to your consumers.
Deployment topology:
Run Strapi and Spring Boot as separate services, ideally orchestrated with Docker Compose:
[External clients]
↓
[Reverse proxy / [nginx](https://nginx.org/)]
↓
[Spring Boot JVM container] ← HTTP → [Strapi Node.js container] ← → [PostgreSQL]Strapi defaults to port 1337 and Spring Boot to 8080. For service-to-service calls within the Docker network, use API Tokens rather than admin JWTs.
You now have a working pattern for connecting Java applications to Strapi v5. From here, explore adding webhook listeners for real-time content updates, implementing response caching with Spring's @Cacheable, or extending the blog reader with search and pagination. The Strapi documentation covers additional features like custom controllers and middleware that can further optimize your integration.
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and Java documentation.
Integrating Java with Strapi combines Java's reliable backend capabilities with Strapi's flexible content management. This results in a powerful, scalable system that can manage and deliver dynamic content efficiently while leveraging Java's robustness for business logic and performance.
To integrate Java with Strapi, you need Strapi v4 or later, Node.js (version 18 or higher), npm or yarn, and Java SDK 8 or higher. Familiarity with RESTful and GraphQL APIs and basic knowledge of Java and JavaScript is recommended.
You can use Java's built-in HttpClient or libraries like HttpClient, RestTemplate, or Retrofit to communicate with Strapi's REST or GraphQL APIs. Set up the correct headers (such as the Authorization token) and structure your requests to match Strapi's API endpoints.
Yes, you can integrate Java Spring Boot with Strapi for full-stack applications. Spring Boot handles user authentication, service logic, and API management, while Strapi manages content and provides API endpoints, offering a robust, scalable solution for modern web applications.
Authentication between Java and Strapi can be managed using JWT (JSON Web Tokens). You can generate a JWT from Strapi upon user login, then send it with subsequent API requests to authenticate and authorize access to protected content.
Data synchronization between Java and Strapi is handled via API calls. You can make GET, POST, PUT, and DELETE requests to interact with Strapi's content. Use Strapi's lifecycle hooks or webhooks to keep data synced in real-time.
Java's high-performance capabilities, combined with Strapi’s automatic API generation, result in faster processing and response times. This setup is particularly effective for high-traffic applications that require fast data handling and high concurrency, benefiting from Java's efficient memory management and Strapi’s flexible APIs.
Yes, Strapi supports both REST and GraphQL APIs, allowing you to choose the most suitable option for your Java application. REST APIs are ideal for simple queries, while GraphQL provides more flexibility for complex data retrieval and real-time content fetching.
To scale your Java and Strapi integration, you can use horizontal scaling, adding more servers to handle traffic. Strapi's flexible architecture allows for easy addition of content types and features without affecting performance. Additionally, caching and load balancing can further optimize scaling.
Best practices include securely storing API tokens, optimizing database queries, using error handling middleware, setting up proper authentication mechanisms, and implementing logging and monitoring for better visibility. You should also ensure the efficient use of APIs and optimize frontend data fetching strategies.
You can find support through Strapi’s Open Office Hours, where experts assist with integration questions, or visit the Strapi community forums, GitHub issues, and documentation for troubleshooting tips and solutions. The official Strapi documentation and Java resources are also valuable for troubleshooting integration issues.