Demo repository for the conference talk "An Opinionated Guide to Bulletproof APIs with Java".
This project demonstrates 5 essential patterns for building production-grade APIs using Jakarta EE 11 and MicroProfile 7 β with zero runtime-specific code. The same WAR runs on Quarkus, Open Liberty, and Helidon.
| # | Pattern | Package | Key Tech |
|---|---|---|---|
| 1 | The Gatekeepers β Input sanitization, validation, auditing | gatekeepers |
ContainerRequestFilter, ReaderInterceptor, @Valid, @NameBinding |
| 2 | The Security Shield β JWT, RBAC, request signatures | security |
MicroProfile JWT, @RolesAllowed, HMAC-SHA256 signature verification |
| 3 | The Lens β Observability, tracing, correlation IDs | observability |
OpenTelemetry, MicroProfile Health, X-Request-Id |
| 4 | The Living Contract β OpenAPI as source of truth | openapi |
MicroProfile OpenAPI, OASFilter |
| 5 | The Evolution β API versioning (URI + header-based) | versioning, resource.v1, resource.v2 |
@PreMatching filter, URI rewriting |
| β | Bonus: Sane Error Handling β RFC 9457 Problem Details | error |
ExceptionMapper, application/problem+json |
- Java 21 (LTS)
- Jakarta EE 11 (Web Profile)
- MicroProfile 7.0 (JWT, OpenAPI, Health, Config, Telemetry)
- OpenTelemetry (tracing)
- Maven (build)
- Java 21+
- Maven 3.9+
- Docker (required for integration tests, optional for Jaeger traces)
mvn clean compile quarkus:dev -PquarkusThe app starts at http://localhost:8080. Quarkus dev mode enables live reload.
mvn clean package liberty:dev -PlibertyOpen Liberty dev mode at http://localhost:8080.
mvn clean package -Phelidon
java -jar target/confapi.jarcurl http://localhost:8080/api/v1/sessions | jqGenerate test tokens using the included script:
# Generate an ORGANIZER token (can create/update/delete)
./generate-jwt.sh ORGANIZER
# Generate a SPEAKER token
./generate-jwt.sh SPEAKER
# Generate an ATTENDEE token (read-only)
./generate-jwt.sh ATTENDEEUse the token:
TOKEN=$(./generate-jwt.sh ORGANIZER 2>/dev/null | grep -E '^ey')
curl -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"New Session","abstract":"Description","level":"BEGINNER","speakerId":"spk-duke","startTime":"2026-10-16T11:00:00","durationMinutes":50}' \
http://localhost:8080/api/v1/sessionsStart the Jaeger trace collector:
docker compose up -dThen make some API calls. View traces at: http://localhost:16686
Health checks:
curl http://localhost:8080/health # All checks
curl http://localhost:8080/health/live # Liveness
curl http://localhost:8080/health/ready # ReadinessThe OpenAPI spec is auto-generated from code annotations:
# YAML format
curl http://localhost:8080/openapi
# JSON format
curl http://localhost:8080/openapi?format=jsonMost runtimes also serve Swagger UI at /openapi/ui or /q/swagger-ui.
Two strategies running side by side:
# URI-based (explicit)
curl http://localhost:8080/api/v1/sessions # Flat DTOs
curl http://localhost:8080/api/v2/sessions # Enriched with embedded speaker/room
# Header-based (transparent routing)
curl -H "X-API-Version: 2" http://localhost:8080/api/sessions
curl -H "Accept: application/json; version=2" http://localhost:8080/api/sessionsAll errors return RFC 9457 Problem Details (application/problem+json):
{
"type": "urn:problem-type:validation-error",
"title": "Validation Failed",
"status": 400,
"detail": "The request body or parameters failed validation.",
"extensions": {
"violations": [
{ "field": "title", "message": "Title is required" }
]
}
}src/main/java/com/mehmandarov/confapi/
βββ ApiApplication.java # JAX-RS app + OpenAPI + JWT config
βββ domain/ # Entity classes (Session, Speaker, Room)
βββ dto/ # Versioned DTOs (V1 flat, V2 enriched)
βββ repository/ # In-memory stores (ConcurrentHashMap)
βββ resource/
β βββ v1/ # V1 endpoints (CRUD)
β βββ v2/ # V2 endpoints (enriched reads)
βββ gatekeepers/ # Ch1: Sanitization, audit, validation, ReaderInterceptor
βββ security/ # Ch2: JWT claims + HMAC signature verification
βββ observability/ # Ch3: Tracing, correlation IDs, health checks
βββ openapi/ # Ch4: OASFilter for OpenAPI enrichment
βββ versioning/ # Ch5: Header-based version routing
βββ error/ # Bonus: RFC 9457 Problem Details mappers
src/test/java/com/mehmandarov/confapi/
βββ unit/ # 48 unit tests (no container, no Docker)
β βββ Ch1_GatekeepersTest.java # Sanitization, ReaderInterceptor, @NoProfanity
β βββ Ch2_SecurityShieldTest.java # HMAC-SHA256, constant-time comparison
β βββ Ch3_ObservabilityTest.java # Correlation ID, health checks
β βββ Ch5_EvolutionTest.java # V1/V2 DTOs, version detection
β βββ Ch6_ErrorHandlingTest.java # RFC 9457 ProblemDetail builder
βββ support/ # Test infrastructure (runtime-agnostic)
β βββ ConfApiContainer.java # Singleton Testcontainer (Docker image per runtime)
β βββ ConfApiExtension.java # JUnit 5 extension (starts container, configures REST Assured)
β βββ TestTokens.java # Real RS256 JWT generator (nimbus-jose-jwt)
βββ Ch1_GatekeepersIT.java # IT: sanitization, validation, public reads
βββ Ch2_SecurityShieldIT.java # IT: 401/403/201 with real JWT tokens
βββ Ch3_ObservabilityIT.java # IT: health checks, X-Request-Id correlation
βββ Ch4_LivingContractIT.java # IT: OpenAPI spec structure, security scheme, OASFilter
βββ Ch5_EvolutionIT.java # IT: URI + header-based versioning
βββ Ch6_ErrorHandlingIT.java # IT: 404/400/401/403 β RFC 9457 Problem Details
- Java 21+ and Maven 3.9+ β for all tests
- Docker β required for integration tests (Testcontainers builds and runs the app in a container)
Colima / non-default Docker socket? Set
DOCKER_HOSTbefore running:export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"
mvn test -PquarkusThese run in ~3 seconds. No container, no Docker. Pure JUnit 5.
mvn verify -PquarkusThis:
- Compiles and runs 40 unit tests (surefire)
- Packages the application
- Builds a Docker image from the build output (Testcontainers)
- Starts the container, waits for
/api/v1/sessionsto return 200 - Runs 34 integration tests against the live container (failsafe)
- Stops the container
The IT tests are completely runtime-agnostic β they contain zero Quarkus, Liberty, or Helidon imports. The architecture:
| Component | Role |
|---|---|
ConfApiContainer |
Singleton Testcontainer. Builds a Docker image from the Maven build output and starts it once per test run. |
ConfApiExtension |
JUnit 5 @ExtendWith β starts the container and points REST Assured at its dynamic port. |
TestTokens |
Generates real RS256 JWT tokens (signed with test-private-key.pem) for security tests. The container validates them through the standard MicroProfile JWT pipeline β no mocks. |
To switch runtimes, only the Docker image builder changes β the tests stay identical:
mvn verify -Pquarkus # Quarkus (default)
mvn verify -Pliberty -Druntime.profile=liberty # Open Liberty (future)
mvn verify -Phelidon -Druntime.profile=helidon # Helidon (future)The integration tests share a single container across all 6 IT classes. This keeps the total IT run under 10 seconds (container starts once in ~3 s, then 34 tests run against it).
Trade-off: tests share mutable state. A session created in Ch2_SecurityShieldIT is visible to later tests. This is acceptable for a demo API with seed data, but if full isolation is required, replace the singleton in ConfApiContainer with a per-class container β at the cost of ~3 s startup per IT class (~18 s total instead of ~7 s).
| Phase | Tests | Docker? |
|---|---|---|
Unit (mvn test) |
40 | No |
Integration (mvn verify) |
34 | Yes |
| Total | 74 |
Apache 2.0