-
Ensure docker is installed using
docker --version -
Within the parent directory build the maven package and run the docker container
mvn package clean
docker compose build --no-cache
docker compose up -d- Go to:
- http://localhost:8080 for the landing page
- http://localhost:8080/fhir/swagger-ui/ for Swagger UI
- To add packages to HAPI see
fhir.implementationguideswithin hapi.application.yaml, or use the helper script:
python3 update-packages.pyThe docker container hapiproject/hapi:vx.x.x is the official HL7 release of the hapi-fhir-jpaserver-starter.
The hapi.application.yaml is a copy of application.yaml which has been modified to include the necessary packages.
This project adds authenticated terminology support and XML Swagger UI visibility to the HAPI FHIR JPA Server Docker image.
It proxies terminology operations ($expand, $validate-code, $lookup, $translate) to the NHS Ontology Server (Ontoserver) using OAuth2 client credentials, and patches the OpenAPI spec so application/fhir+xml appears alongside JSON in the Swagger UI.
Out of the box, HAPI FHIR cannot expand SNOMED CT ValueSets because it does not hold the terminology content locally. This interceptor forwards those requests to a remote terminology server that does, injecting a Bearer token automatically.
Client → HAPI FHIR → TerminologyOperationInterceptor → NHS Ontology Server
↑
TerminologyInterceptor
(fetches & caches OAuth2 token)
Browser → Swagger UI → /fhir/api-docs → OpenApiCustomizer → patched spec (XML added)
- A request hits HAPI FHIR for a terminology operation (e.g.
POST /fhir/ValueSet/$expand) TerminologyOperationInterceptor(a Spring servlet filter) catches it before HAPI processes it- It calls
TerminologyInterceptor.getBearerToken()which fetches a token from the NHS auth server using OAuth2 Client Credentials — or returns the cached token if it's still valid - The original request is forwarded to the NHS Ontology Server with the token in the
Authorizationheader - The response is streamed directly back to the client
HAPI's local database is never consulted for these operations.
src/main/java/com/nhs/
├── TerminologyInterceptor.java # OAuth2 token management
├── TerminologyOperationInterceptor.java # Servlet filter — proxies terminology requests
├── TerminologyFilterConfig.java # Registers servlet filters with Spring Boot
└── OpenApiCustomizer.java # Patches OpenAPI spec to add XML content type
Implements HAPI's IClientInterceptor. Manages the OAuth2 token lifecycle:
- Fetches a token from
ONTO_AUTH_URLusingONTO_CLIENT_IDandONTO_CLIENT_SECRET - Caches the token in memory and refreshes it 60 seconds before expiry
- Thread-safe via double-checked locking
- Exposes
getBearerToken()for use by the proxy filter
A Spring OncePerRequestFilter that runs at the servlet level, before HAPI touches the request:
- Checks if the request URI contains a terminology operation (
$expand,$validate-code,$lookup,$translate) - Strips the
/fhirprefix and forwards the request toONTO_SERVER_URL - Injects the Bearer token from
TerminologyInterceptor - Streams the remote response back to the caller unchanged
A Spring @Configuration class that explicitly registers both servlet filters. Required because the JAR is loaded dynamically by HAPI's class loader, so @Component alone is not sufficient.
A Spring OncePerRequestFilter that intercepts the /fhir/api-docs response:
- Detects whether the spec is YAML (default) or JSON
- Parses it with Jackson, adds
application/fhir+xmlwhereverapplication/fhir+jsonappears - Writes the patched spec back so Swagger UI shows XML as a content type option on all endpoints
- Docker and Docker Compose
- Java 17+ and Maven (for building)
- Request a system-to-system account (from The NHS England terminology server)
All secrets are stored in a .env file in the project root. Never commit this file to version control. A .env.example is provided as a template.
ONTO_AUTH_URL=https://ontology.nhs.uk/authorisation/auth/realms/nhs-digital-terminology/protocol/openid-connect/token
ONTO_CLIENT_ID=your-client-id
ONTO_CLIENT_SECRET=your-client-secret
ONTO_SERVER_URL=https://ontology.nhs.uk/production1/fhir
| Variable | Description |
|---|---|
ONTO_AUTH_URL |
OAuth2 token endpoint on the NHS auth server |
ONTO_CLIENT_ID |
Your OAuth2 client ID |
ONTO_CLIENT_SECRET |
Your OAuth2 client secret |
ONTO_SERVER_URL |
Base URL of the NHS Ontology Server FHIR endpoint |
git clone <repo>
cd <repo>
cp .env.example .env
# Fill in your credentials in .envEdit package.json to add FHIR packages, then run:
python3 update-packages.pymvn packageThis produces target/term-interceptor-1.0.jar.
docker compose up -dDocker Compose will:
- Pull the HAPI FHIR image if not already present
- Mount the JAR into
/app/extra-classes/where HAPI's class loader picks it up - Mount
hapi.application.yamlas the server config - Inject all environment variables from
.env
docker compose logs fhir | grep -i "proxy\|token\|interceptor\|openapi"On the first terminology request you should see:
[CONFIG] Terminology proxy filter registered for /fhir/*
[CONFIG] OpenAPI XML customizer filter registered.
[PROXY] Intercepted POST /fhir/ValueSet/$expand — forwarding to https://ontology.nhs.uk/production1/fhir
[INTERCEPTOR] Refreshing OAuth2 token...
[INTERCEPTOR] Token refreshed. Expires in 300 s.
[OPENAPI] Injected application/fhir+xml into spec.
curl -X POST "http://localhost:8080/fhir/ValueSet/\$expand" \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType": "Parameters",
"parameter": [{
"name": "url",
"valueUri": "http://snomed.info/sct?fhir_vs=isa/73211009"
}]
}'Open http://localhost:8080/fhir/swagger-ui/ — all endpoints should now show application/fhir+xml in the request body dropdown alongside JSON.
When a new HAPI FHIR version is released:
- Check available Docker tags:
curl -s "https://registry.hub.docker.com/v2/repositories/hapiproject/hapi/tags?page_size=10" \
| python3 -m json.tool | grep '"name"'- Update the image tag in
docker-compose.yml(use the non-tomcatvariant):
image: "hapiproject/hapi:v9.x.x-1"- Update the HAPI version in
pom.xml:
<version>9.x.x</version>- Rebuild and redeploy:
mvn package
docker compose down
docker compose up -dNo Java code changes should be required for routine version bumps.
Container won't start
docker compose logs fhir | tail -50Env vars not reaching the container
- Ensure
.envis in the same directory asdocker-compose.yml - No spaces around
=in.env(useKEY=valuenotKEY = value) - Verify with:
docker inspect fhir-server | grep -A 20 '"Env"'
401 from auth server
- Double-check
ONTO_AUTH_URLis the full token endpoint URL - Verify
ONTO_CLIENT_IDandONTO_CLIENT_SECRETare correct
ValueSet too large error
- This comes from Ontoserver, not HAPI — the interceptor is working correctly
- Use a more specific SNOMED concept or add a
countparameter to limit results
XML not showing in Swagger UI
- Check
[OPENAPI] Injectedappears in logs after visiting Swagger UI - Hard refresh the browser (Ctrl+Shift+R) to clear the cached spec
Rebuilding after code changes
rm -rf target
mvn package
docker compose down
docker compose up -d