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 | jqPrerequisites: openssl and uuidgen (or /proc/sys/kernel/random/uuid). Both ship with macOS and most Linux distros. On Windows, run the script under WSL or Git Bash.
Generate 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 ATTENDEEπ¨ First-run side effect: if
/tmp/confapi_private.pemdoesn't exist, the script generates a fresh RSA key pair and overwritessrc/main/resources/META-INF/publicKey.pem. You must rebuild and restart the runtime afterwards so it picks up the new public key. Subsequent runs reuse the existing key and have no side effects.
Use the token from the command line:
# The JWT is the only line in the script's output that starts with "ey"
# (RS256 tokens always begin with the base64-encoded header {"alg":"RS256",...})
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/sessionsOr use it from the .http files: copy the token into http/http-client.env.json under jwt_organizer / jwt_speaker / jwt_attendee, then fire requests from any .http file.
The project uses two independent RSA key pairs, on purpose:
| Used by | Private key | Public key (verifier side) |
|---|---|---|
Runtime (live app + generate-jwt.sh) |
/tmp/confapi_private.pem (generated on first script run) |
src/main/resources/META-INF/publicKey.pem |
| Integration tests | src/test/resources/test-private-key.pem (committed) |
Loaded by the test container via mp.jwt.verify.publickey.location from test resources |
This means a token generated with ./generate-jwt.sh works against the running app, not against the test container β and vice versa. If you regenerate the runtime key, tests are unaffected.
Start 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" }
]
}
}Ready-to-run requests for every demo live in http/. Open them in JetBrains IDEs or VS Code (REST Client extension) and fire requests one click at a time.
| File | Use it for |
|---|---|
http/demos.http |
Presenter's walkthrough β every demo from the talk, in slide order |
http/sessions.http, speakers.http, rooms.http |
Per-resource CRUD reference |
http/security.http, signatures.http |
JWT/RBAC + HMAC signature flows |
http/versioning.http |
URI vs. header-based versioning |
http/health.http, errors.http |
Health probes and RFC 9457 Problem Details |
See http/README.md for the full catalogue, setup steps, and a chapter β file map.
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