{"id":"xl5fw1afaypmuwq","title":"Java and Spring Content APIs for Headless Frontends","slug":"java-spring-content-apis-headless","summary":"Spring Boot still powers serious content backends. I expose JSON that React and static sites can cache without fighting JPA on every request.","imageUrl":"https://briancrabtree.me/images/journal-java-spring-content-apis-headless.webp","category":"Backend","date":"2026-04-06T18:00:00.000Z","featured":false,"likes":22,"author":"Brian Crabtree","content":"<h2>When Java still makes sense</h2>\n\n<p>Enterprise integrations, existing Oracle or Postgres schemas, and teams with decade of Spring expertise are valid reasons. Rewriting to Node for fashion hurts. I wire headless JSON in front of proven domains instead of rip-and-replace.</p>\n\n<p>Content APIs are read-heavy. Optimize reads even if write paths stay ornate.</p>\n\n<p>Flyway migrations versioned with app releases prevent schema drift between content API and monolith admin.</p>\n\n<pre><code>@GetMapping(\"/api/posts\")\npublic Page&lt;PostDto&gt; list(@RequestParam(defaultValue = \"0\") int page) {\n  return postService.findPublished(PageRequest.of(page, 20));\n}</code></pre>\n\n<h2>Controller shape for frontends</h2>\n\n<p>DTOs decouple entities from JSON. Never serialize lazy Hibernate proxies accidentally. I use @Transactional on service layer, return immutable records or DTOs from controllers, and map with MapStruct or hand mappers when simple.</p>\n\n<p>Version /api/v1 and document breaking changes. OpenAPI generated from annotations keeps React types honest.</p>\n\n<p>Jackson serialization of dates as ISO strings with explicit ZoneOffset avoids off-by-one day bugs in CMS schedules.</p>\n\n<h2>Caching and pagination</h2>\n\n<p>Spring Cache on hot lists, Redis for session and rate limits, HTTP cache headers on public JSON. Pageable with stable sort keys; avoid OFFSET on huge tables when cursor possible.</p>\n\n<p>ETag based on updatedAt timestamp field saves bandwidth for mobile apps polling feeds.</p>\n\n<p>Rate limiting at API gateway protects Spring from scrapers even when controllers are fast.</p>\n\n<h2>Security stack</h2>\n\n<p>Spring Security with JWT or session cookies depending on client. CORS configured per origin, not star with credentials. Input validation with Bean Validation annotations mirrored in OpenAPI schema.</p>\n\n<p>I expose If-None-Match on article list endpoints so mobile apps skip body download when unchanged.</p>\n\n<p>When I audit backend stacks, Security stack is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point.</p>\n\n<h2>Operational reality</h2>\n\n<p>JVM heap and GC matter under load. I profile before micro-optimizing JSON serializers. Virtual threads in modern Spring help IO bound workloads. Still monitor connection pools to database.</p>\n\n<p>Docker images with layered JAR, health checks on actuator, structured JSON logs for Loki or CloudWatch.</p>\n\n<p>Spring Boot 3 native image is not default for every team; JVM tuning plus horizontal scale is boring and works.</p>\n\n<h2>Frontends consuming the API</h2>\n\n<p>React or Astro fetches at build for static paths, ISR or on-demand revalidation when content changes. Webhooks from CMS trigger cache purge at CDN. Firebase is not the only backend; Spring plus CDN is a legitimate architecture.</p>\n\n<p>Contract tests with Pact between frontend and Spring catch field renames before production.</p>\n\n<p>When I audit backend stacks, Frontends consuming the API is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point.</p>\n\n<h2>Engagement</h2>\n\n<p>If you run Spring and need a read model your marketing site can consume, send entity diagram and SLAs. I design endpoints for one round trip and honest cache, not fifty REST nouns mirroring tables.</p>\n\n<p>Observability: trace id in MDC flows to JSON logs and HTTP response header for support tickets.</p>\n\n<p>When I audit backend stacks, Engagement is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point. For a related angle I keep coming back to, see <a href=\"/journal/spring-boot-content-api-pagination/\">Spring Boot Content API Pagination for Headless Frontends</a>.</p>","tags":["java","spring","headless"],"views":64}