HAPI FHIR Plain Server Implementation
Quick Start
Basic Server Setup
public class MyFhirServer extends RestfulServer {
@Override
protected void initialize() throws ServletException {
setFhirContext(FhirContext.forR4());
setResourceProviders(List.of(
new PatientResourceProvider(),
new ObservationResourceProvider()
));
registerInterceptor(new ResponseHighlighterInterceptor());
}
}
Minimal Resource Provider
public class PatientResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() { return Patient.class; }
@Read
public Patient read(@IdParam IdType id) {
return loadPatient(id.getIdPart());
}
@Search
public List<Patient> search(
@OptionalParam(name = Patient.SP_FAMILY) StringParam family) {
return searchPatients(family);
}
}
Server Architecture
Component Hierarchy
RestfulServer (Servlet)
├── FhirContext (version-specific, expensive to create - reuse)
├── Resource Providers (IResourceProvider per resource type)
├── Plain Providers (cross-resource operations)
├── Interceptors (request/response hooks)
└── Configuration (paging, address strategy, encoding)
Server vs Storage Distinction
- SERVER_xxx pointcuts: Request/response lifecycle hooks
- STORAGE_xxx pointcuts: Data persistence hooks (must be triggered manually in Plain Server)
Core Patterns
CRUD Operations
| Operation | Annotation | Key Parameters |
|---|---|---|
| Read | @Read |
@IdParam IdType |
| VRead | @Read(version=true) |
@IdParam IdType (with version) |
| Create | @Create |
@ResourceParam Patient |
| Update | @Update |
@IdParam, @ResourceParam |
| Delete | @Delete |
@IdParam IdType |
| Patch | @Patch |
@IdParam, PatchTypeEnum, @ResourceParam String |
Conditional Operations
Add @ConditionalUrlParam String to support conditional create/update/delete:
@Update
public MethodOutcome update(
@ResourceParam Patient patient,
@IdParam IdType id,
@ConditionalUrlParam String conditional) {
if (conditional != null) {
// Find by search criteria in conditional URL
}
// Perform update
}
MethodOutcome Return
MethodOutcome outcome = new MethodOutcome();
outcome.setId(new IdType("Patient", "123", "1"));
outcome.setCreated(true); // For create operations
return outcome;
Search Implementation
Parameter Types
| Type | Class | Example URL |
|---|---|---|
| String | StringParam |
?family=Smith |
| Token | TokenParam |
?identifier=mrn|123 |
| Date | DateParam/DateRangeParam |
?date=ge2020-01-01 |
| Reference | ReferenceParam |
?subject=Patient/123 |
| Quantity | QuantityParam |
?value-quantity=gt5||kg |
Multi-Value Logic
// OR logic (comma-separated): ?family=Smith,Jones
@OptionalParam(name = "family") StringOrListParam families
// AND logic (repeated param): ?family=Smith&family=Jones
@OptionalParam(name = "family") StringAndListParam families
Paging with IBundleProvider
For large results, return IBundleProvider instead of List<IBaseResource>:
@Search
public IBundleProvider search(...) {
List<String> ids = findMatchingIds();
return new SimpleBundleProvider(ids) {
@Override
public List<IBaseResource> getResources(int from, int to) {
return loadResources(ids.subList(from, Math.min(to, ids.size())));
}
};
}
Configure paging provider on server:
FifoMemoryPagingProvider paging = new FifoMemoryPagingProvider(10);
paging.setDefaultPageSize(20);
paging.setMaximumPageSize(100);
setPagingProvider(paging);
Extended Operations ($operations)
@Operation(name = "$everything", idempotent = true)
public Bundle everything(
@IdParam IdType patientId,
@OperationParam(name = "start") DateType start,
@OperationParam(name = "end") DateType end) {
// Return bundle with patient and related resources
}
- Instance-level: Include
@IdParam - Type-level: No
@IdParam, register on resource provider - Server-level: Register on plain provider (no
IResourceProvider) idempotent = true: Allows HTTP GET (only for primitive params)
Interceptors
Registration
registerInterceptor(new LoggingInterceptor());
registerInterceptor(new ResponseHighlighterInterceptor());
Custom Interceptor
@Interceptor
public class MyInterceptor {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void preHandle(RequestDetails details, HttpServletRequest request) {
// Before request processing
}
@Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
public void postHandle(RequestDetails details, IBaseResource resource) {
// After response generated
}
}
Key Pointcuts
SERVER_INCOMING_REQUEST_PRE_PROCESSED: Earliest hookSERVER_INCOMING_REQUEST_PRE_HANDLED: After handler selectedSERVER_OUTGOING_RESPONSE: Before response sentSERVER_HANDLE_EXCEPTION: On any exception
Essential Built-in Interceptors
| Interceptor | Purpose |
|---|---|
LoggingInterceptor |
Request/response logging |
ResponseHighlighterInterceptor |
HTML view for browsers |
RequestValidatingInterceptor |
Validate incoming resources |
ResponseValidatingInterceptor |
Validate outgoing resources |
CorsInterceptor |
CORS support |
ExceptionHandlingInterceptor |
Custom error responses |
Security
Authentication Pattern
@Interceptor
public class AuthInterceptor {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void authenticate(RequestDetails details, HttpServletRequest request) {
String auth = request.getHeader("Authorization");
if (!validateToken(auth)) {
throw new AuthenticationException("Invalid credentials");
}
details.getUserData().put("user", extractUser(auth));
}
}
Authorisation with AuthorizationInterceptor
public class MyAuthInterceptor extends AuthorizationInterceptor {
@Override
public List<IAuthRule> buildRuleList(RequestDetails details) {
String userId = (String) details.getUserData().get("user");
return new RuleBuilder()
.allow().read().allResources()
.inCompartment("Patient", new IdType("Patient", userId))
.andThen()
.allow().write().allResources()
.inCompartment("Patient", new IdType("Patient", userId))
.andThen()
.denyAll()
.build();
}
}
Validation
Setup
ValidationSupportChain chain = new ValidationSupportChain(
new DefaultProfileValidationSupport(ctx),
new InMemoryTerminologyServerValidationSupport(ctx),
new CommonCodeSystemsTerminologyService(ctx)
);
FhirInstanceValidator validator = new FhirInstanceValidator(chain);
RequestValidatingInterceptor interceptor = new RequestValidatingInterceptor();
interceptor.addValidatorModule(validator);
interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
registerInterceptor(interceptor);
Profile Validation
Add NpmPackageValidationSupport for implementation guide validation:
NpmPackageValidationSupport npm = new NpmPackageValidationSupport(ctx);
npm.loadPackageFromClasspath("classpath:package/us.core.tgz");
Exception Handling
| Exception | HTTP Status | Use Case |
|---|---|---|
ResourceNotFoundException |
404 | Resource not found |
InvalidRequestException |
400 | Bad request parameters |
UnprocessableEntityException |
422 | Validation failure |
AuthenticationException |
401 | Auth required |
ForbiddenOperationException |
403 | Access denied |
ResourceVersionConflictException |
409 | Version mismatch |
InternalErrorException |
500 | Server error |
Configuration
Server Address Strategy
// For reverse proxy
setServerAddressStrategy(new HardcodedServerAddressStrategy("https://api.example.com/fhir"));
// For Apache mod_proxy
setServerAddressStrategy(new ApacheProxyAddressStrategy(true));
Response Defaults
setDefaultResponseEncoding(EncodingEnum.JSON);
setDefaultPrettyPrint(true);
Multitenancy
setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
// Access via: details.getTenantId()
Detailed References
- Resource Providers: See references/resource-providers.md
- Search Operations: See references/search.md
- Interceptors: See references/interceptors.md
- Security: See references/security.md
- Extended Operations: See references/operations.md
Converted and distributed by TomeVault — claim your Tome and manage your conversions.