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 |
| β | Bonus: Unknown JSON Fields β what each provider does with extra fields | unknownfields |
JSON-B vs Jackson defaults, @JsonIgnoreProperties |
| β | Bonus: Binary Uploads β receiving files as multipart or raw body | upload |
Jakarta REST EntityPart, application/octet-stream |
| β | Bonus: Resumable Uploads (TUS) β pause/resume large uploads over flaky networks | upload.tus |
Pure jakarta.ws.rs implementation of the TUS 1.0 core protocol |
- Java 25 (LTS)
- Jakarta EE 11 (Web Profile)
- MicroProfile 7.0 (JWT, OpenAPI, Health, Config, Telemetry)
- OpenTelemetry (tracing)
- Maven (build)
- Java 25+
- Maven 3.9+
- Docker (required for integration tests, container runs, and Jaeger traces)
You can either run the app locally on the JVM (best for dev β live reload, fast feedback) or fully in Docker alongside Jaeger via Docker Compose.
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 | jqWhen the app runs locally, OpenTelemetry exports traces to
http://localhost:4317(the value baked intomicroprofile-config.properties). Start just Jaeger withdocker compose up -d jaegerto collect them β see Observability below.
The repo ships with Dockerfiles under docker/ (one folder per runtime β see docker/README.md) and a docker-compose.yml that brings up the app and Jaeger together on a shared network. The compose confapi service builds from docker/quarkus/Dockerfile (multi-stage, JDK 25 β JRE 25, Quarkus fast-jar).
# Build the image and start everything
docker compose up -d --build
# Tail app logs
docker compose logs -f confapi
# Verify it works
curl http://localhost:8080/api/v1/sessions | jq
# Stop everything
docker compose downServices:
| Service | URL | Purpose |
|---|---|---|
confapi |
http://localhost:8080 | The API |
jaeger |
http://localhost:16686 | Jaeger UI (search service confapi) |
How the networking works: Compose puts both services on a shared bridge network where containers resolve each other by service name. The compose file overrides OTEL_EXPORTER_OTLP_ENDPOINT to http://jaeger:4317 for the confapi container, so the in-repo microprofile-config.properties value (localhost:4317) is untouched β local non-Docker dev keeps working unchanged.
Rebuild after code changes:
docker compose up -d --build confapiThe shipped Compose setup targets the Quarkus profile via
docker/quarkus/Dockerfile. Liberty/Helidon equivalents would live alongside it asdocker/liberty/Dockerfileetc. β the Testcontainers.itvariants are already in place.
If you want the app containerized and still get live reload on file change, use the opt-in dev compose profile. It runs mvn quarkus:dev inside a container (docker/quarkus/Dockerfile.dev) with the project directory bind-mounted β edit a source file on the host and Quarkus recompiles on the next request.
# Start Jaeger + the dev-mode container (first run downloads dependencies β be patient)
docker compose --profile dev up -d --build confapi-dev
# Tail the logs and watch the reloads happen
docker compose logs -f confapi-dev
# Try it: edit any file under src/, then hit an endpoint
curl http://localhost:8080/api/v1/sessions | jq
# Stop everything (--profile dev also stops confapi-dev)
docker compose --profile dev downHow it works:
- The project root is bind-mounted at
/workspace, so the container compiles the files you edit on the host. No image rebuild, no restart. - The Maven repository lives in a named volume (
confapi-m2), so dependencies are downloaded once and reused across container restarts. - Reload is triggered on the next HTTP request β Quarkus dev mode checks file timestamps per request, which works fine over bind mounts (no inotify needed).
- Don't run
confapiandconfapi-devat the same time β both map port 8080.
Note: dev mode in a container is for demos and "works on my machine" debugging. For day-to-day development, plain
mvn quarkus:devon the host (option A) is faster.
Prerequisites: 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.
Traces are exported via OTLP/gRPC to a collector. Pick whichever startup matches how you're running the app:
# App running locally on the JVM β start only Jaeger
docker compose up -d jaeger
# App running in Docker β app + Jaeger come up together
docker compose up -d --buildThen make some API calls and view traces at http://localhost:16686 (service name: confapi).
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.
Bonus demo of the TUS 1.0 resumable-upload protocol, implemented in pure jakarta.ws.rs so it runs on all three runtimes without change. Lives in com.mehmandarov.confapi.upload.tus.
Endpoints (all under /api/tus, @PermitAll β no JWT required for the demo):
| Method + Path | Purpose |
|---|---|
OPTIONS /api/tus |
Capability discovery (Tus-Version, Tus-Extension: creation) |
POST /api/tus |
Create an upload (send Upload-Length: <bytes>); returns 201 + Location: /api/tus/{id} |
HEAD /api/tus/{id} |
Ask where to resume β returns Upload-Offset |
PATCH /api/tus/{id} |
Append a chunk at Upload-Offset (Content-Type: application/offset+octet-stream); returns new offset |
DELETE /api/tus/{id} |
Terminate a single upload (TUS Termination extension) - deletes the partial file on the server |
GET /api/tus |
Demo-only listing of every file in /tmp/tus-uploads/ (name, size, in-progress flag, declared length). The demo page renders this as a live table. |
DELETE /api/tus |
Demo-only nuke: wipes every file and sidecar under /tmp/tus-uploads/; the demo page has a "Purge server" button that hits this |
GET /api/tus/demo |
Serves the browser demo page (see below) |
Try it in the browser. With the app running (docker compose up -d --build or any local runtime), open:
Pick a file, watch the progress bar, click Pause / Resume. Behind the scenes it uses tus-js-client to chunk the file (5 MB per PATCH) and to resume automatically on the next start() after a pause or network glitch. Ready-made raw HTTP requests for the same protocol are in http/tus.http.
Where uploaded files land. Chunks are appended to ${java.io.tmpdir}/tus-uploads/<uuid> β i.e. /tmp/tus-uploads/ inside the container. To inspect them mid-upload:
docker compose exec confapi ls -la /tmp/tus-uploads # or confapi-dev in dev profileDemo-only caveats (called out in the TusResource Javadoc):
- The
UPLOAD_LENGTHSmap is in-memory and/tmpis not persisted bydocker-compose.yml, so restarting the container drops both the files and their declared lengths. A real deployment would persist metadata (DB) and files (mounted volume or object store). TusCorsFilteropensAccess-Control-Allow-Origin: *on/api/tus*so the HTML page also works when opened directly from disk (file://β¦/src/main/resources/webdemo/tus-upload-demo.html) against a locally-running app. Narrow that origin in production.
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
βββ unknownfields/ # Bonus: extra-field demo endpoint + lean Room DTO
βββ upload/ # Bonus: binary uploads (multipart EntityPart + raw body)
βββ tus/ # Bonus: resumable uploads (TUS 1.0) + browser demo page
src/test/java/com/mehmandarov/confapi/
βββ unit/ # 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
β βββ Ch7_UnknownFieldsTest.java # JSON-B vs Jackson unknown-field defaults
βββ 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
βββ Ch8_UploadIT.java # IT (bonus): multipart + raw binary uploads
- Java 25+ 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 44 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 38 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 IT classes. This keeps the total IT run under 10 seconds (container starts once in ~3 s, then 38 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) |
44 | No |
Integration (mvn verify) |
38 | Yes |
| Total | 82 |
Apache 2.0