Skip to content

mehmandarov/api-guide-java

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ›‘οΈ An Opinionated Guide to Bulletproof APIs with Java

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.

πŸ“‹ What's Inside

# 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

πŸ—οΈ Tech Stack

  • Java 21 (LTS)
  • Jakarta EE 11 (Web Profile)
  • MicroProfile 7.0 (JWT, OpenAPI, Health, Config, Telemetry)
  • OpenTelemetry (tracing)
  • Maven (build)

πŸš€ Running the Application

Prerequisites

  • Java 21+
  • Maven 3.9+
  • Docker (required for integration tests, optional for Jaeger traces)

Option 1: Quarkus

mvn clean compile quarkus:dev -Pquarkus

The app starts at http://localhost:8080. Quarkus dev mode enables live reload.

Option 2: Open Liberty

mvn clean package liberty:dev -Pliberty

Open Liberty dev mode at http://localhost:8080.

Option 3: Helidon

mvn clean package -Phelidon
java -jar target/confapi.jar

Verify it works

curl http://localhost:8080/api/v1/sessions | jq

πŸ” JWT Authentication

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.pem doesn't exist, the script generates a fresh RSA key pair and overwrites src/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/sessions

Or 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.

Two key pairs β€” runtime vs. tests

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.

πŸ“Š Observability

Start the Jaeger trace collector:

docker compose up -d

Then 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   # Readiness

πŸ“– OpenAPI

The OpenAPI spec is auto-generated from code annotations:

# YAML format
curl http://localhost:8080/openapi

# JSON format
curl http://localhost:8080/openapi?format=json

Most runtimes also serve Swagger UI at /openapi/ui or /q/swagger-ui.

πŸ”„ API Versioning

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/sessions

⚠️ Error Handling

All 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" }
    ]
  }
}

🌐 Try the API β€” .http files

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.

πŸ“ Project Structure

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

πŸ§ͺ Running the Tests

Prerequisites

  • 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_HOST before running:

export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"

Unit Tests Only (48 tests β€” fast, no Docker)

mvn test -Pquarkus

These run in ~3 seconds. No container, no Docker. Pure JUnit 5.

Full Suite: Unit + Integration (74 tests)

mvn verify -Pquarkus

This:

  1. Compiles and runs 40 unit tests (surefire)
  2. Packages the application
  3. Builds a Docker image from the build output (Testcontainers)
  4. Starts the container, waits for /api/v1/sessions to return 200
  5. Runs 34 integration tests against the live container (failsafe)
  6. Stops the container

How the Integration Tests Work

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)

Test Isolation vs. Startup Time

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).

Test Counts

Phase Tests Docker?
Unit (mvn test) 40 No
Integration (mvn verify) 34 Yes
Total 74

πŸ“ License

Apache 2.0

About

Demo repository for the conference talk "An Opinionated Guide to Bulletproof APIs with Java".

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors