Spring Boot JSONPlaceholder Showcase

A compact, production-minded REST API that turns the original JSONPlaceholder experiment into a public portfolio project. It demonstrates how I structure a modern Spring Boot application: clean HTTP contracts, secure write operations, JPA persistence, cache-aware reads, external API integration, tested behaviour, and environment-driven configuration.

The application is intentionally a runnable API rather than a full product. It is small enough to review quickly, but its patterns scale to admin portals, booking systems, internal tools, marketplace back ends, and integration services.

What this demonstrates

Capability Implementation
Modern Java and dependencies Java 21, Spring Boot 3.5.16, Maven Wrapper 3.9.11, dependency versions managed by Spring Boot's BOM
REST API design POST, GET, PUT, and DELETE endpoints with a stable page envelope and proper 201, 204, 400, 401, 404, 409, and 503 responses
Spring Data JPA UUID primary keys, derived search query, pagination, audit timestamps, transaction boundaries, and optimistic locking
Caching Bounded Caffeine cache for individual read operations; update, delete, and import paths keep it coherent
Security Stateless HTTP Basic authentication for writes, BCrypt password hashing, least-privilege route rules
External integration Spring's RestClient, explicit timeout, separate remote DTO, failure translation, and idempotent JSONPlaceholder import
Configuration management Validated typed configuration, local H2 defaults, a PostgreSQL production profile, environment-variable overrides, no machine paths, no committed production credentials
Reliability RFC 9457-style ProblemDetail errors, input validation, health endpoint, and integration tests

Architecture

HTTP client
    │
    ├── PostController ── request/response DTOs
    │        │
    │        └── PostService ── transactions, cache policy, import workflow
    │                 ├── PostRepository ── Spring Data JPA ── H2 / production database
    │                 └── JsonPlaceholderClient ── RestClient ── JSONPlaceholder
    │
    └── ApiExceptionHandler ── consistent problem responses

The controller does not contain persistence or business logic. The service owns the transaction and cache rules, the repository owns database access, and the integration client owns the remote contract. This makes each responsibility straightforward to test and replace.

Run locally

Prerequisites: JDK 21+ and an internet connection the first time Maven downloads dependencies.

./mvnw.cmd spring-boot:run

The API starts on http://localhost:8080. Check its health at GET /actuator/health.

The checked-in account is deliberately a local demo account:

username: demo-editor
password: changeit

It is not a secret and must be replaced outside local development. Use environment variables instead of editing configuration or committing credentials:

$env:APP_EDITOR_USERNAME = "your-editor"
$env:APP_EDITOR_PASSWORD = "use-a-real-secret-manager-in-production"
./mvnw.cmd spring-boot:run

For a deployed service, the prod profile switches to PostgreSQL and requires APP_DB_URL, APP_DB_USERNAME, APP_DB_PASSWORD, APP_EDITOR_USERNAME, and APP_EDITOR_PASSWORD. It has no secret defaults:

$env:SPRING_PROFILES_ACTIVE = "prod"
$env:APP_DB_URL = "jdbc:postgresql://host:5432/postshowcase"
$env:APP_DB_USERNAME = "application_user"
$env:APP_DB_PASSWORD = "store-this-outside-git"
./mvnw.cmd spring-boot:run

For a real deployment, run schema migrations with Flyway or Liquibase and supply all values from the hosting platform's secret store.

API quick start

Public read endpoints require no login.

curl http://localhost:8080/api/posts
curl "http://localhost:8080/api/posts?title=spring&page=0&size=10"

Create, update, delete, and import operations require the editor account.

curl -u demo-editor:changeit -X POST http://localhost:8080/api/posts `
  -H "Content-Type: application/json" `
  -d '{"authorId":1,"title":"Spring Boot showcase","body":"A practical API example."}'

curl -u demo-editor:changeit -X POST http://localhost:8080/api/posts/import/jsonplaceholder

An update includes the resource's version. This is a deliberate optimistic-locking contract: if another editor saves first, the API returns 409 POST_VERSION_CONFLICT instead of overwriting their work.

{
  "title": "Revised title",
  "body": "Revised body",
  "version": 0
}

The JSONPlaceholder import limit, remote base URL, and timeout are application properties. Imported source IDs are recorded, so repeating the import safely skips records already imported.

Test and build

./mvnw.cmd test
./mvnw.cmd package

The integration test verifies the public/private route split, authenticated creation, validation error shape, title search, and duplicate-safe import. It substitutes the remote client, so the test suite does not rely on a live third-party service.

Learning journal

1. Prefer a standalone executable API over a legacy WAR

The previous project used a Spring Boot 2.3 WAR with JSP/Thymeleaf remnants and Java 8. This rewrite uses a runnable JAR, Java 21, Spring Boot 3.5.16, and the Maven Wrapper. This removes servlet-container deployment assumptions and keeps the repository focused on back-end skills.

2. Keep API, domain, persistence, and remote models separate

CreatePostRequest, UpdatePostRequest, and PostResponse describe the public API. Post describes a database row. JsonPlaceholderPost describes a third-party response. Those models happen to look similar today, but they will evolve for different reasons; separating them prevents accidental breaking changes.

3. Validate at the edge and transact in the service

Jakarta Validation rejects bad JSON before business work starts. PostService then provides read-only and write transaction boundaries. The entity is intentionally free of controller annotations, which makes it reusable outside HTTP.

4. Cache only a narrow, safe read path

Only GET /api/posts/{id} is cached. Search results are dynamic and therefore intentionally not cached. Write methods update or evict the cached item, and imports clear the small cache. The Caffeine cache is size-bounded and expires after ten minutes, avoiding unbounded memory growth and stale values that linger forever.

5. Secure mutation, not a toy login screen

Anyone may read sample posts. Every operation that changes data requires the EDITOR role. The application uses stateless HTTP Basic authentication because it is easy to inspect in a small API; a browser or mobile application would normally sit behind OAuth2/OIDC or a gateway. Password configuration comes from environment variables and is BCrypt-hashed in memory.

6. Make integration failures part of the contract

The remote API is called through Spring RestClient with a configurable timeout. A client error becomes an application exception and then a 503 problem response; it never becomes an unstructured stack trace. The import has a source identifier and skips known records, making retries safe.

7. Use actionable errors and tests

The exception advice returns a predictable problem document with a machine-readable code and field-level validation errors. The integration tests execute real MVC, security, JPA, cache, and transaction wiring. Only the outbound HTTP boundary is mocked, keeping tests deterministic.

File-by-file learning notes

Every Java source file carries a dated header and closing marker in the style of the larger reference application. The source notes are intentionally concise; this table is the fuller reading guide.

File Why it exists / learning focus
ShowcaseApplication Enables bootstrapping, configuration-property discovery, and JPA auditing in one explicit place.
config/ApiProperties Binds and validates remote API settings as typed values.
config/SecurityProperties Keeps login values external to source code.
config/CacheConfig Defines a bounded, expiring Caffeine cache.
config/RestClientConfig Names and configures the remote HTTP client with connection and read timeouts.
config/SecurityConfig Defines stateless route authorization and BCrypt-backed editor credentials.
integration/JsonPlaceholderPost Isolates the third-party JSON contract.
integration/JsonPlaceholderClient Converts remote HTTP calls into a small application-facing API.
post/Post Maps the persistence model with UUID, audit fields, source ID, and optimistic version.
post/PostRepository Shows pagination and case-insensitive search without handwritten SQL.
post/CreatePostRequest Defines and validates the create contract.
post/UpdatePostRequest Defines the update contract and requires the expected version.
post/PostResponse Shields API consumers from JPA implementation details.
post/PageResponse Keeps paginated JSON stable even if framework serialization changes.
post/ImportResult Reports an import outcome compactly.
post/PostService Centralizes transactions, cache coherence, conflict detection, and idempotent import rules.
post/PostController Translates HTTP verbs, status codes, URI creation, and pagination into service calls.
support/PostNotFoundException Represents a missing domain resource.
support/PostVersionConflictException Represents a safe concurrent-update failure.
support/UpstreamServiceException Prevents third-party errors leaking through the API.
support/ApiExceptionHandler Turns expected failures into a consistent public error format.
test/PostApiIntegrationTest Tests behaviour across the actual Spring web stack without a live remote dependency.

What I can build from this foundation

This structure is suitable for secure CRUD APIs, internal dashboards, workflow back ends, integrations with external SaaS APIs, catalogue or content services, and the server side of web or mobile applications. Depending on the product, I can extend it with PostgreSQL/MySQL, schema migrations, OAuth2/OIDC, role and permission models, file uploads, scheduled work, queue consumers, email, metrics/tracing, Docker, CI, and cloud deployment configuration.

Description
Portfolio showcase: modern Spring Boot app with Thymeleaf UI, Spring Data JPA, Caffeine caching, Spring Security, validation, and tests.
Readme Unlicense 137 KiB
v1.0.0 Latest
2026-08-19 14:02:57 +00:00
Languages
Java 67.2%
HTML 25.4%
JavaScript 7.4%