1. Introducing HAPI FHIR
HAPI FHIR is the most popular open source Java library for building FHIR Server and Client. HAPI FHIR JPA Server provides a complete FHIR Server with persistence layer using JPA/Hibernate.
| Ingredients | Description |
|---|---|
| HAPI FHIR Core | Parser, model classes, client/server framework |
| JPA Server | Full FHIR server with database persistence |
| Validation | Profile-based validation engine |
| CLI | Command-line tool for migration, upload |
| FHIR Version | Supports DSTU2, STU3, R4, R4B, R5 |
2. Initialize Project with Spring Boot
Maven Dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<hapi.fhir.version>7.4.0</hapi.fhir.version>
</properties>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-starter</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
</dependencies>
Application Properties
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/hapi_fhir
username: hapi
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: false
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
hapi:
fhir:
fhir_version: R5
server_address: http://localhost:8080/fhir
allow_multiple_delete: true
allow_external_references: true
default_page_size: 20
max_page_size: 200
validation:
requests_enabled: true
responses_enabled: false
3. Configure FHIR Server
@Configuration
public class FhirServerConfig {
@Bean
public RestfulServer restfulServer(
ApplicationContext context,
FhirContext fhirContext) {
RestfulServer server = new RestfulServer(fhirContext);
server.setDefaultResponseEncoding(EncodingEnum.JSON);
server.setDefaultPrettyPrint(true);
// Register resource providers
server.registerProviders(
context.getBean(PatientResourceProvider.class),
context.getBean(ObservationResourceProvider.class),
context.getBean(EncounterResourceProvider.class)
);
// Register interceptors
server.registerInterceptor(new ResponseHighlighterInterceptor());
server.registerInterceptor(new CorsInterceptor());
server.registerInterceptor(context.getBean(AuditInterceptor.class));
return server;
}
@Bean
public FhirContext fhirContext() {
return FhirContext.forR5();
}
}
4. Resource Providers
@Component
public class PatientResourceProvider implements IResourceProvider {
@Autowired
private PatientRepository patientRepo;
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Read
public Patient read(@IdParam IdType theId) {
return patientRepo.findById(theId.getIdPart())
.orElseThrow(() -> new ResourceNotFoundException(theId));
}
@Create
public MethodOutcome create(@ResourceParam Patient patient) {
// Validate trước khi lưu
ValidationResult result = validator.validateWithResult(patient);
if (!result.isSuccessful()) {
throw new UnprocessableEntityException(
result.toOperationOutcome());
}
Patient saved = patientRepo.save(patient);
return new MethodOutcome()
.setId(saved.getIdElement())
.setCreated(true);
}
@Search
public List<Patient> searchByName(
@RequiredParam(name = Patient.SP_NAME) StringParam name) {
return patientRepo.findByNameContaining(name.getValue());
}
@Search
public List<Patient> searchByIdentifier(
@RequiredParam(name = Patient.SP_IDENTIFIER)
TokenParam identifier) {
return patientRepo.findByIdentifier(
identifier.getSystem(), identifier.getValue());
}
@Operation(name = "$everything", idempotent = true)
public Bundle patientEverything(@IdParam IdType patientId) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.SEARCHSET);
Patient patient = read(patientId);
bundle.addEntry().setResource(patient);
// Thêm Encounters, Observations, Conditions...
encounterRepo.findByPatient(patientId.getIdPart())
.forEach(e -> bundle.addEntry().setResource(e));
observationRepo.findBySubject(patientId.getIdPart())
.forEach(o -> bundle.addEntry().setResource(o));
return bundle;
}
}
5. Interceptors
HAPI FHIR used Interceptor pattern to hook into the request's lifecycle.
@Component
public class AuditInterceptor {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void logIncomingRequest(
RequestDetails requestDetails) {
log.info("FHIR Request: {} {} from {}",
requestDetails.getRequestType(),
requestDetails.getCompleteUrl(),
requestDetails.getAttribute("remoteAddr"));
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void auditCreate(IBaseResource resource,
RequestDetails requestDetails) {
AuditEvent audit = new AuditEvent();
audit.setAction(AuditEvent.AuditEventAction.C);
audit.setRecorded(new Date());
AuditEvent.AuditEventAgentComponent agent =
audit.addAgent();
agent.setRequestor(true);
// Set agent from auth context
AuditEvent.AuditEventEntityComponent entity =
audit.addEntity();
entity.setWhat(new Reference(
resource.getIdElement().toUnqualifiedVersionless()));
auditRepo.save(audit);
}
}
Validation Interceptor
@Component
public class ValidationInterceptor {
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
public void validateResource(IBaseResource resource) {
FhirValidator validator = fhirContext.newValidator();
// Thêm profile validation
IValidatorModule module = new FhirInstanceValidator(
validationSupport);
validator.registerValidatorModule(module);
ValidationResult result = validator.validateWithResult(resource);
if (!result.isSuccessful()) {
OperationOutcome oo = (OperationOutcome)
result.toOperationOutcome();
throw new UnprocessableEntityException(
fhirContext, oo);
}
}
}
6. Custom Search Parameters
{
"resourceType": "SearchParameter",
"url": "http://xdev.asia/fhir/SearchParameter/patient-cccd",
"name": "cccd",
"status": "active",
"description": "Tìm kiếm bệnh nhân theo số CCCD",
"code": "cccd",
"base": ["Patient"],
"type": "token",
"expression": "Patient.identifier.where(system='http://xdev.asia/fhir/sid/cccd').value"
}
Register search parameters:
curl -X POST http://localhost:8080/fhir/SearchParameter \
-H "Content-Type: application/fhir+json" \
-d @search-parameter-cccd.json
# Reindex để áp dụng
curl -X POST http://localhost:8080/fhir/$reindex \
-H "Content-Type: application/fhir+json" \
-d '{"resourceType":"Parameters","parameter":[{"name":"url","valueString":"Patient"}]}'
7. Bulk Data Export
# Khởi tạo export Patient và Observation
curl -X GET 'http://localhost:8080/fhir/$export' \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Content-Type: application/fhir+json" \
--data '{
"resourceType": "Parameters",
"parameter": [
{"name": "_type", "valueString": "Patient,Observation"},
{"name": "_outputFormat", "valueString": "application/fhir+ndjson"}
]
}'
# Response Header: Content-Location: http://localhost:8080/fhir/$export-poll/abc123
# Kiểm tra trạng thái
curl http://localhost:8080/fhir/\$export-poll/abc123
# Download kết quả khi hoàn tất
curl -o patients.ndjson http://localhost:8080/fhir/bulk/abc123/Patient
8. Docker Deployment
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/hapi-fhir-server.jar app.jar
ENV JAVA_OPTS="-Xmx2g -Xms1g"
ENV SPRING_PROFILES_ACTIVE=production
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
services:
fhir-server:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/hapi_fhir
- SPRING_DATASOURCE_USERNAME=hapi
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
- HAPI_FHIR_SERVER_ADDRESS=https://fhir.xdev.asia/fhir
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/fhir/metadata"]
interval: 30s
timeout: 10s
retries: 5
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: hapi_fhir
POSTGRES_USER: hapi
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hapi"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
9. Performance Tuning
| Configuration | Recommended value | Description |
|---|---|---|
| hibernate.jdbc.batch_size | 20-50 | Batch insert/update |
| max_page_size | 200 | Limit the results returned |
| reuse_cached_search_results | 60000 (ms) | Cache search results |
| JVM Heap | 2-4 GB | Depending on the number of resources |
| Connection pool | 20-50 | HikariCP connections |
| PostgreSQL shared_buffers | 25% RAM | Database buffer |
10. Summary
HAPI FHIR JPA Server — Full-featured FHIR server built on Spring Boot + PostgreSQL
Resource Providers — Implement CRUD, search, custom operations
Interceptors — Hook into request lifecycle for audit, validation, security
Custom Search Parameters — Expand search according to needs (CCCD, health insurance)
Bulk Data Export — Export large amounts of data in NDJSON format
Docker — Production deployment with docker-compose