Real-World Patterns
This page shows how the FHIRPath Go library fits into production Go services. Each pattern is self-contained and demonstrates a common integration scenario.
HTTP Middleware for FHIR® Search Parameter Evaluation
A FHIR® server often needs to evaluate search parameters against stored resources. The following middleware evaluates a FHIRPath expression provided in a query parameter and returns the result as JSON.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"github.com/gofhir/fhirpath"
)
// FHIRPathHandler evaluates a FHIRPath expression against a posted FHIR resource.
//
// Usage:
// POST /fhirpath?expr=Patient.name.family
// Body: { "resourceType": "Patient", ... }
//
// Response:
// { "result": ["Smith"], "count": 1 }
func FHIRPathHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
expr := r.URL.Query().Get("expr")
if expr == "" {
http.Error(w, `missing "expr" query parameter`, http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Validate that the body is valid JSON
if !json.Valid(body) {
http.Error(w, "body is not valid JSON", http.StatusBadRequest)
return
}
// Use EvaluateCached for automatic expression caching
result, err := fhirpath.EvaluateCached(body, expr)
if err != nil {
http.Error(w, fmt.Sprintf("evaluation error: %v", err), http.StatusUnprocessableEntity)
return
}
// Convert the result to a JSON-serializable form
items := make([]interface{}, len(result))
for i, v := range result {
items[i] = v.String()
}
response := map[string]interface{}{
"result": items,
"count": len(items),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/fhirpath", FHIRPathHandler)
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Testing the Middleware
curl -X POST "http://localhost:8080/fhirpath?expr=Patient.name.family" \
-H "Content-Type: application/json" \
-d '{"resourceType":"Patient","name":[{"family":"Smith","given":["John"]}]}'Expected response:
{"count":1,"result":["Smith"]}Batch Processing Pipeline
When you need to evaluate expressions against a large number of resources (for example, extracting search indices), precompile the expression once and reuse it:
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"sync"
"github.com/gofhir/fhirpath"
)
// ExtractionRule defines a field to extract from a resource.
type ExtractionRule struct {
Name string
Expression *fhirpath.Expression
}
// ExtractionResult holds the extracted values for one resource.
type ExtractionResult struct {
ResourceID string
Fields map[string][]string
Errors []string
}
// BatchExtractor precompiles expressions and runs them against many resources.
type BatchExtractor struct {
rules []ExtractionRule
}
// NewBatchExtractor creates an extractor from a map of name -> FHIRPath expression strings.
func NewBatchExtractor(expressions map[string]string) (*BatchExtractor, error) {
rules := make([]ExtractionRule, 0, len(expressions))
for name, expr := range expressions {
compiled, err := fhirpath.Compile(expr)
if err != nil {
return nil, fmt.Errorf("failed to compile %q: %w", name, err)
}
rules = append(rules, ExtractionRule{Name: name, Expression: compiled})
}
return &BatchExtractor{rules: rules}, nil
}
// Extract evaluates all rules against a single resource.
func (b *BatchExtractor) Extract(resource []byte) ExtractionResult {
result := ExtractionResult{
Fields: make(map[string][]string),
}
// Try to get the resource ID
id, _ := fhirpath.EvaluateToString(resource, "id")
result.ResourceID = id
for _, rule := range b.rules {
col, err := rule.Expression.Evaluate(resource)
if err != nil {
result.Errors = append(result.Errors,
fmt.Sprintf("%s: %v", rule.Name, err))
continue
}
values := make([]string, len(col))
for i, v := range col {
values[i] = v.String()
}
result.Fields[rule.Name] = values
}
return result
}
// ExtractBatch processes multiple resources concurrently.
func (b *BatchExtractor) ExtractBatch(resources [][]byte, workers int) []ExtractionResult {
results := make([]ExtractionResult, len(resources))
var wg sync.WaitGroup
sem := make(chan struct{}, workers)
for i, resource := range resources {
wg.Add(1)
sem <- struct{}{} // acquire semaphore slot
go func(idx int, res []byte) {
defer wg.Done()
defer func() { <-sem }() // release semaphore slot
results[idx] = b.Extract(res)
}(i, resource)
}
wg.Wait()
return results
}
func main() {
// Define extraction rules for Patient resources
extractor, err := NewBatchExtractor(map[string]string{
"family": "Patient.name.where(use = 'official').family",
"given": "Patient.name.where(use = 'official').given",
"birthDate": "Patient.birthDate",
"city": "Patient.address.where(use = 'home').city",
"phone": "Patient.telecom.where(system = 'phone').value",
})
if err != nil {
log.Fatal(err)
}
// Simulate a batch of patients
patients := [][]byte{
[]byte(`{
"resourceType": "Patient", "id": "p1",
"name": [{"use": "official", "family": "Smith", "given": ["Alice"]}],
"birthDate": "1990-05-15",
"address": [{"use": "home", "city": "Portland"}],
"telecom": [{"system": "phone", "value": "555-0001"}]
}`),
[]byte(`{
"resourceType": "Patient", "id": "p2",
"name": [{"use": "official", "family": "Johnson", "given": ["Bob", "James"]}],
"birthDate": "1985-11-20",
"address": [{"use": "home", "city": "Seattle"}],
"telecom": [{"system": "phone", "value": "555-0002"}, {"system": "email", "value": "bob@example.com"}]
}`),
[]byte(`{
"resourceType": "Patient", "id": "p3",
"name": [{"use": "official", "family": "Williams", "given": ["Carol"]}],
"birthDate": "1978-03-08",
"address": [{"use": "work", "city": "Denver"}],
"telecom": [{"system": "email", "value": "carol@example.com"}]
}`),
}
// Process with 4 concurrent workers
results := extractor.ExtractBatch(patients, 4)
// Output results
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
for _, r := range results {
fmt.Printf("--- Patient %s ---\n", r.ResourceID)
enc.Encode(r.Fields)
if len(r.Errors) > 0 {
fmt.Println(" Errors:", r.Errors)
}
}
}Expression Validation Endpoint
Before storing a user-provided FHIRPath expression (e.g., in a configuration database), you should validate that it compiles correctly. This endpoint does just that:
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gofhir/fhirpath"
)
type ValidateRequest struct {
Expression string `json:"expression"`
}
type ValidateResponse struct {
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
}
func ValidateExpressionHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
var req ValidateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
if req.Expression == "" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ValidateResponse{
Valid: false,
Error: "expression is required",
})
return
}
// Try to compile the expression
_, err := fhirpath.Compile(req.Expression)
resp := ValidateResponse{Valid: err == nil}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
http.HandleFunc("/validate-expression", ValidateExpressionHandler)
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Testing the Validation Endpoint
Valid expression:
curl -X POST http://localhost:8080/validate-expression \
-H "Content-Type: application/json" \
-d '{"expression": "Patient.name.where(use = '\''official'\'').family"}'{"valid": true}Invalid expression:
curl -X POST http://localhost:8080/validate-expression \
-H "Content-Type: application/json" \
-d '{"expression": "Patient.name.where(!!!"}'{"valid": false, "error": "...parse error details..."}Error Handling Patterns in Production
Robust error handling is essential for production code. Here are the patterns you should follow.
Distinguishing Compilation from Evaluation Errors
package main
import (
"errors"
"fmt"
"log"
"github.com/gofhir/fhirpath"
)
func safeEvaluate(resource []byte, expr string) {
// Step 1: Compile the expression (catches syntax errors)
compiled, err := fhirpath.Compile(expr)
if err != nil {
log.Printf("COMPILE ERROR for %q: %v", expr, err)
return
}
// Step 2: Evaluate against the resource (catches runtime errors)
result, err := compiled.Evaluate(resource)
if err != nil {
log.Printf("EVAL ERROR for %q: %v", expr, err)
return
}
fmt.Printf("Expression: %s\n", expr)
fmt.Printf("Result: %v (count: %d)\n\n", result, len(result))
}
func main() {
patient := []byte(`{
"resourceType": "Patient",
"id": "pat-err",
"name": [{"family": "TestPatient"}]
}`)
// Valid expression
safeEvaluate(patient, "Patient.name.family")
// Syntax error
safeEvaluate(patient, "Patient.name.!!!")
// Valid expression but path does not exist -- returns empty, not an error
safeEvaluate(patient, "Patient.maritalStatus.coding.code")
}Wrapping Errors with Context
func extractField(resource []byte, resourceID, fieldName, expr string) (string, error) {
result, err := fhirpath.EvaluateCached(resource, expr)
if err != nil {
return "", fmt.Errorf(
"failed to extract %s from resource %s: %w",
fieldName, resourceID, err,
)
}
if result.Empty() {
return "", nil // field is absent -- not an error in FHIR
}
return result[0].String(), nil
}Graceful Degradation
In many production scenarios you want to extract as much data as possible, logging failures rather than aborting:
package main
import (
"fmt"
"log"
"github.com/gofhir/fhirpath"
)
// FieldSpec defines a field to extract.
type FieldSpec struct {
Name string
Expression string
Required bool
}
// ExtractFields extracts all specified fields, collecting errors for failed ones.
func ExtractFields(resource []byte, specs []FieldSpec) (map[string]string, []error) {
fields := make(map[string]string)
var errs []error
for _, spec := range specs {
value, err := fhirpath.EvaluateToString(resource, spec.Expression)
if err != nil {
errs = append(errs, fmt.Errorf("field %s: %w", spec.Name, err))
continue
}
if value == "" && spec.Required {
errs = append(errs,
fmt.Errorf("field %s: required but empty", spec.Name))
continue
}
fields[spec.Name] = value
}
return fields, errs
}
func main() {
patient := []byte(`{
"resourceType": "Patient",
"id": "pat-degrade",
"name": [{"family": "Torres", "given": ["Sofia"]}],
"gender": "female"
}`)
specs := []FieldSpec{
{Name: "family", Expression: "Patient.name.family", Required: true},
{Name: "given", Expression: "Patient.name.given.first()", Required: true},
{Name: "gender", Expression: "Patient.gender", Required: false},
{Name: "birthDate", Expression: "Patient.birthDate", Required: true},
{Name: "phone", Expression: "Patient.telecom.where(system = 'phone').value", Required: false},
}
fields, errs := ExtractFields(patient, specs)
fmt.Println("Extracted fields:")
for k, v := range fields {
fmt.Printf(" %s = %s\n", k, v)
}
if len(errs) > 0 {
fmt.Println("\nWarnings/Errors:")
for _, e := range errs {
log.Printf(" %v", e)
}
}
}Monitoring Cache Performance
In long-running services, monitor the expression cache to ensure good hit rates:
package main
import (
"fmt"
"log"
"net/http"
"encoding/json"
"github.com/gofhir/fhirpath"
)
// CacheStatsHandler exposes expression cache metrics.
func CacheStatsHandler(w http.ResponseWriter, r *http.Request) {
stats := fhirpath.DefaultCache.Stats()
response := map[string]interface{}{
"size": stats.Size,
"limit": stats.Limit,
"hits": stats.Hits,
"misses": stats.Misses,
"hit_rate": fhirpath.DefaultCache.HitRate(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
// Simulate some cache usage
patient := []byte(`{"resourceType":"Patient","name":[{"family":"Doe"}]}`)
for i := 0; i < 100; i++ {
fhirpath.EvaluateCached(patient, "Patient.name.family")
}
for i := 0; i < 50; i++ {
fhirpath.EvaluateCached(patient, "Patient.id")
}
stats := fhirpath.DefaultCache.Stats()
fmt.Printf("Cache size: %d / %d\n", stats.Size, stats.Limit)
fmt.Printf("Hits: %d, Misses: %d\n", stats.Hits, stats.Misses)
fmt.Printf("Hit rate: %.1f%%\n", fhirpath.DefaultCache.HitRate())
// Expose as an HTTP endpoint
http.HandleFunc("/metrics/cache", CacheStatsHandler)
log.Println("Cache metrics at :8080/metrics/cache")
log.Fatal(http.ListenAndServe(":8080", nil))
}Summary of Best Practices
| Practice | Why |
|---|---|
Use EvaluateCached in request handlers | Avoids recompiling the same expression on every request |
Use Compile + Expression.Evaluate in batch jobs | Compile once, evaluate many times for maximum throughput |
| Validate user-supplied expressions before storing | Catches syntax errors early, before they cause runtime failures |
| Log evaluation errors but do not abort | FHIR® data is inherently variable; missing fields are normal |
Monitor DefaultCache.Stats() | Ensures the cache is sized correctly for your workload |
| Use concurrent workers with a semaphore | Exploits Go’s concurrency without overwhelming the CPU |
| Separate compile errors from evaluation errors | Gives clearer diagnostics to operators and users |