API Design for Longitudinal Health Records
Overview
This document outlines the API endpoints and controller enhancements required for the LHR system. The design maintains 100% backward compatibility while adding powerful new longitudinal health record capabilities.
Core Principle: Enhance existing APIs with LHR data, add new endpoints for LHR-specific features, ensure all existing functionality continues to work unchanged.
Enhanced Existing APIs
1. Enhanced Encounter APIs
Enhanced Encounter Index
# GET /api/encounters
# Enhanced response to include LHR summary data
{
"encounters": [
{
"id": 123,
"patient_id": 456,
"doctor_id": 789,
# ... existing fields unchanged ...
# NEW: LHR enhancement fields (optional, only if available)
"encounter_type": "routine",
"health_score_snapshot": 78.5,
# Note: reconciliation_completed removed - using simple continuity,
"lhr_enhanced": true,
"enhanced_complaints_count": 3,
"enhanced_medicines_count": 2,
"clinical_alerts_count": 1
}
],
# NEW: Optional LHR summary for the patient list
"lhr_summary": {
"total_encounters": 15,
"avg_health_score": 76.2,
"patients_with_alerts": 3,
"chronic_medications_count": 12
}
}Enhanced Encounter Show
# GET /api/encounters/:id?include_lhr=true
# Enhanced response with comprehensive LHR data
{
"encounter": {
"id": 123,
# ... existing fields unchanged ...
# NEW: LHR fields
"encounter_type": "routine",
"health_score_snapshot": 78.5,
"reconciliation_completed": false,
# Enhanced complaints with longitudinal data
"complaints": [
{
"id": 1,
"name": "Hypertension",
# ... existing fields ...
# NEW: LHR fields
"problem_status": "chronic",
"severity_level": "moderate",
"onset_date": "2023-06-15",
"chronic_condition": true,
"linked_complaint_ids": [45, 67],
"affects_health_score": true,
"clinical_codes": {
"primary": {
"system": "ICD10",
"code": "I10",
"display": "Essential hypertension"
},
"confidence": 0.95
}
}
],
# Enhanced medicines with continuity data
"medicines": [
{
"id": 1,
"medicine_name": "Lisinopril",
# ... existing fields ...
# NEW: LHR fields
"medication_status": "active",
"prescription_type": "chronic",
"start_date": "2023-06-15",
"end_date": null,
"drug_interactions": [
{
"severity": "moderate",
"description": "Monitor potassium levels",
"interacting_drug": "Potassium supplements"
}
],
# Note: reconciliation_status removed,
"rxnorm_code": {
"code": "29046",
"display": "Lisinopril"
}
}
]
},
# NEW: LHR insights (only when include_lhr=true)
"lhr_insights": {
"health_score_trend": "improving",
"clinical_alerts": [
{
"type": "drug_interaction",
"severity": "moderate",
"message": "Potential drug interactions detected",
"count": 1
}
],
"patient_summary": {
"active_problems_count": 2,
"chronic_conditions_count": 1,
"current_medications_count": 3,
"last_health_score": 78.5,
"health_score_date": "2024-01-15"
}
}
}Enhanced Encounter Update
# PATCH /api/encounters/:id
# Existing functionality preserved, new LHR fields can be updated
# Request body (backward compatible)
{
"encounter": {
# ... existing fields work as before ...
# NEW: UI-driven fields from encounter creation
"clinic_patient_id": 456,
"doctor_id": 789,
"visit_type": "scheduled", # walkin/scheduled/telemedicine/home_visit
"urgency": "routine", # routine/urgent/emergency
"chief_complaint": "Follow-up for hypertension management",
"encounter_type": "followup", # routine/emergency/followup/telemedicine
# NEW: Optional LHR fields
# Note: reconciliation_completed removed,
# Enhanced complaint updates
"complaints": [
{
"id": 1,
# ... existing fields ...
"name": "Hypertension",
"problem_status": "chronic",
"severity_level": "moderate",
"onset_date": "2023-06-15",
"chronic_condition": true
}
],
# Enhanced medicine updates
"medicines": [
{
"id": 1,
# ... existing fields ...
"medication_status": "active",
"prescription_type": "chronic",
"reconciliation_status": "verified"
}
]
}
}
# Response includes LHR enhancements
{
"success": true,
"encounter": { /* enhanced encounter data */ },
"lhr_processing_queued": true # Background LHR enhancement queued
}New LHR-Specific APIs
2. Longitudinal Health Summary APIs
Get Patient Health Summary
# GET /api/patients/:patient_id/health_summary
# Comprehensive longitudinal health overview with UI-friendly structure
{
"patient_id": 456,
"generated_at": "2024-01-15T10:30:00Z",
"patient_demographics": {
"name": "John Doe",
"age": 45,
"gender": "Male",
"date_of_birth": "1979-03-15",
"phone": "+91-9876543210",
"email": "john.doe@email.com",
"address": "123 Main Street, Mumbai",
"blood_group": "O+",
"profile_picture": "/api/patients/456/photo"
},
"health_summary": {
# Health Score Card
"health_score_card": {
"overall_score": 78.5,
"trend": "improving",
"components": {
"vitals": { "score": 85, "label": "Vitals" },
"problems": { "score": 70, "label": "Problems" },
"medications": { "score": 82, "label": "Medications" },
"preventive": { "score": 75, "label": "Preventive Care" }
},
"last_calculated": "2024-01-15T08:00:00Z"
},
# Recent Encounters Timeline
"recent_encounters": [
{
"id": 123,
"date": "2024-01-10",
"visit_type": "scheduled",
"urgency": "routine",
"chief_complaint": "Follow-up for hypertension",
"doctor": "Dr. Smith",
"encounter_type": "followup",
"summary": "BP controlled, continue current medications"
}
],
# Active Problems Card
"active_problems": {
"count": 2,
"items": [
{
"name": "Essential Hypertension",
"severity": "moderate",
"onset_date": "2023-06-15",
"duration": "6 months",
"status": "chronic",
"last_updated": "2024-01-10"
}
]
},
# Current Medications Card
"current_medications": {
"count": 3,
"items": [
{
"name": "Lisinopril",
"dosage": "10mg",
"frequency": "Once daily",
"type": "chronic",
"start_date": "2023-06-15",
"prescriber": "Dr. Smith",
"status": "active",
"adherence": "good"
}
]
},
# Vitals Card
"recent_vitals": {
"blood_pressure": {
"value": "125/80",
"unit": "mmHg",
"date": "2024-01-10",
"trend": "improving",
"status": "normal"
},
"weight": {
"value": "75",
"unit": "kg",
"date": "2024-01-10",
"trend": "stable",
"bmi": 24.2
},
"temperature": {
"value": "98.6",
"unit": "°F",
"date": "2024-01-10",
"status": "normal"
}
},
# Summary Statistics
"statistics": {
"total_encounters": 15,
"encounters_this_year": 4,
"days_since_last_visit": 5,
"upcoming_appointments": 1
}
}
}Get Health Score History
# GET /api/patients/:patient_id/health_scores?period=6_months
# Health score trends over time
{
"patient_id": 456,
"period": "6_months",
"health_scores": [
{
"overall_score": 78.5,
"vital_score": 85.0,
"problem_score": 70.0,
"medication_score": 82.0,
"preventive_score": 75.0,
"calculated_date": "2024-01-15",
"encounter_id": 123
}
],
"trends": {
"overall_trend": "improving",
"change_from_previous": "+5.2",
"best_score": 78.5,
"worst_score": 65.3
}
}3. Clinical Coding APIs
Search Clinical Codes
# GET /api/clinical_codes/search?q=hypertension&system=ICD10&category=diagnosis
# Search for clinical codes
{
"query": "hypertension",
"results": [
{
"id": 1,
"code_system": "ICD10",
"code": "I10",
"display_name": "Essential hypertension",
"description": "High blood pressure of unknown cause",
"category": "diagnosis",
"usage_count": 1250,
"confidence": 0.95
}
],
"total_results": 15
}Get Code Suggestions
# GET /api/clinical_codes/suggest?text=patient has high blood pressure&category=diagnosis
# AI-powered code suggestions
{
"text": "patient has high blood pressure",
"suggestions": [
{
"code": {
"id": 1,
"code_system": "ICD10",
"code": "I10",
"display_name": "Essential hypertension"
},
"confidence": 0.92,
"method": "ai_generated",
"reasoning": "Text indicates elevated blood pressure without specified cause"
}
]
}4. AI Transcription APIs
Start Transcription Session
# POST /api/ai_transcription/start_session
# Begin AI transcription for an encounter
# Request
{
"encounter_id": 123,
"settings": {
"auto_populate": true,
"confidence_threshold": 0.8,
"recording_quality": "high"
}
}
# Response
{
"success": true,
"session": {
"session_id": "uuid-string",
"encounter_id": 123,
"status": "active",
"started_at": "2024-01-15T10:00:00Z"
}
}Process Audio Chunk
# POST /api/ai_transcription/process_audio
# Send audio data for transcription
# Request
{
"session_id": "uuid-string",
"audio_data": "base64-encoded-audio",
"format": "webm",
"duration": 5
}
# Response
{
"success": true,
"transcription": {
"id": 456,
"status": "processing",
"confidence": null
}
}Get Live Updates
# GET /api/ai_transcription/live_updates?session_id=uuid-string
# Real-time transcription and suggestions
{
"session_id": "uuid-string",
"status": "active",
"live_transcript": "Patient reports chest pain for the past 2 days...",
"live_suggestions": {
"complaints": [
{
"text": "chest pain",
"confidence": 0.95,
"suggested_codes": ["R06.02"]
}
],
"medications": [
{
"name": "aspirin",
"dosage": "81mg",
"frequency": "daily",
"confidence": 0.87
}
]
}
}5. Medication Continuity APIs
Get Chronic Medications
# GET /api/encounters/:encounter_id/chronic_medications
# Get patient's chronic medications for continuity
{
"encounter_id": 123,
"patient_id": 456,
"chronic_medications": [
{
"id": 789,
"medicine_name": "Metformin",
"generic_name": "Metformin",
"dosage": "500mg",
"frequency": "Twice daily",
"status": "active",
"prescription_type": "chronic",
"start_date": "2023-01-15",
"last_prescribed_date": "2023-12-01",
"prescribed_by": "Dr. Johnson"
}
],
"summary": {
"total_chronic_medications": 3,
"active_chronic_medications": 3
}
}Continue Chronic Medications
# POST /api/encounters/:encounter_id/continue_medications
# Continue selected chronic medications in current encounter
# Request
{
"encounter_id": 123,
"medication_ids": [789, 790, 791]
}
# Response
{
"success": true,
"continued_count": 3,
"medications": [
{
"id": 999,
"medicine_name": "Metformin",
"dosage": "500mg",
"frequency": "Twice daily",
"prescription_type": "chronic",
"parent_prescription_id": 789
}
]
}Controller Implementation
Enhanced Encounters Controller
# parts/doctor/controllers/encounters_controller.rb (Enhanced)
class EncountersController < ApiController
before_action :policy_check
before_action :set_encounter, only: [:show, :update, :lhr_summary, :update_longitudinal]
# EXISTING methods work unchanged
def create
result = Encounters::Create.call(params)
render_result(result, enhanced_encounter_serializer(result.encounter))
end
def index
result = Encounters::Index.call(params)
encounters = result.encounters
# Add LHR summary if requested
response_data = { encounters: enhanced_encounter_serializer(encounters) }
if params[:include_lhr_summary] == 'true'
response_data[:lhr_summary] = generate_lhr_summary_for_list(encounters)
end
render json: response_data
end
def show
# Enhanced show with optional LHR data
serializer_options = {
include_lhr: params[:include_lhr] == 'true'
}
response_data = { encounter: enhanced_encounter_serializer(@encounter, serializer_options) }
if serializer_options[:include_lhr]
response_data[:lhr_insights] = generate_lhr_insights(@encounter)
end
render json: response_data
end
def update
result = Encounters::Update.call(params.merge(encounter: @encounter))
# Queue LHR enhancement if new data was added
if result.success? && has_lhr_updates?(params)
LhrEnhancementJob.perform_later(@encounter.id, 'ENCOUNTER_UPDATED')
end
render_result(result, enhanced_encounter_serializer(result.encounter))
end
# NEW: Get comprehensive LHR summary for an encounter
def lhr_summary
summary = {
health_score: @encounter.health_score_snapshot,
encounter_type: @encounter.encounter_type,
enhanced_complaints: @encounter.lhr_enhanced_complaints.count,
enhanced_medicines: @encounter.lhr_enhanced_medicines.count,
# Note: reconciliation_completed removed,
clinical_alerts: @encounter.clinical_alerts,
ai_transcription_available: @encounter.ai_transcriptions.completed.any?
}
render json: { lhr_summary: summary }
end
# NEW: Update only LHR-specific fields
def update_longitudinal
allowed_fields = params.slice(:encounter_type).to_h
if @encounter.update(allowed_fields)
render json: { success: true, updated_fields: allowed_fields.keys }
else
render json: { errors: @encounter.errors.full_messages }, status: :unprocessable_entity
end
end
private
def enhanced_encounter_serializer(encounter_or_collection, options = {})
if encounter_or_collection.respond_to?(:each)
# Collection
encounter_or_collection.map { |encounter| serialize_single_encounter(encounter, options) }
else
# Single encounter
serialize_single_encounter(encounter_or_collection, options)
end
end
def serialize_single_encounter(encounter, options = {})
base_data = EncounterSerializer.new(encounter).as_json
# Add LHR enhancements if available
if encounter.has_lhr_enhancements?
base_data.merge!(
encounter_type: encounter.encounter_type,
health_score_snapshot: encounter.health_score_snapshot,
# Note: reconciliation_completed removed,
lhr_enhanced: true
)
# Enhanced complaints
base_data[:complaints] = encounter.encounter_complaints.map do |complaint|
complaint_data = ComplaintSerializer.new(complaint).as_json
if complaint.problem_status.present?
complaint_data.merge!(
problem_status: complaint.problem_status,
severity_level: complaint.severity_level,
onset_date: complaint.onset_date,
chronic_condition: complaint.chronic_condition,
clinical_codes: format_clinical_codes(complaint)
)
end
complaint_data
end
# Enhanced medicines
base_data[:medicines] = encounter.encounter_medicines.map do |medicine|
medicine_data = MedicineSerializer.new(medicine).as_json
if medicine.medication_status.present?
medicine_data.merge!(
medication_status: medicine.medication_status,
prescription_type: medicine.prescription_type,
start_date: medicine.start_date,
end_date: medicine.end_date,
drug_interactions: medicine.drug_interactions,
# Note: reconciliation_status removed
)
end
medicine_data
end
end
base_data
end
def generate_lhr_insights(encounter)
{
health_score_trend: encounter.health_score_trend,
clinical_alerts: encounter.clinical_alerts,
patient_summary: {
active_problems_count: encounter.patient.encounter_complaints.where(problem_status: 'active').count,
chronic_conditions_count: encounter.patient.encounter_complaints.where(chronic_condition: true).count,
current_medications_count: encounter.patient.encounter_medicines.where(medication_status: 'active').count,
last_health_score: encounter.patient.patient_health_scores.recent.first&.overall_score,
health_score_date: encounter.patient.patient_health_scores.recent.first&.calculated_date
}
}
end
def has_lhr_updates?(params)
lhr_fields = [:encounter_type]
params.dig(:encounter)&.keys&.any? { |key| lhr_fields.include?(key.to_sym) }
end
endNew LHR Controllers
# app/controllers/api/longitudinal_health_controller.rb
class Api::LongitudinalHealthController < ApiController
before_action :authenticate_user!
before_action :set_patient
def health_summary
service = LongitudinalHealthService.new(@patient.id)
summary = service.build_complete_record
render json: {
patient_id: @patient.id,
generated_at: Time.current,
health_summary: summary
}
end
def health_scores
period = params[:period] || '6_months'
scores = @patient.patient_health_scores
.where('calculated_date >= ?', parse_period(period))
.order(:calculated_date)
render json: {
patient_id: @patient.id,
period: period,
health_scores: scores.map { |score| serialize_health_score(score) },
trends: calculate_score_trends(scores)
}
end
private
def set_patient
@patient = Patient.find(params[:patient_id])
authorize @patient, :show?
end
def parse_period(period)
case period
when '1_month' then 1.month.ago
when '3_months' then 3.months.ago
when '6_months' then 6.months.ago
when '1_year' then 1.year.ago
else 6.months.ago
end
end
endAPI Route Structure
# config/routes.rb (additions)
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
# Enhanced encounter routes (backward compatible)
resources :encounters do
member do
get :lhr_summary
patch :update_longitudinal
end
end
# New LHR-specific routes
resources :patients do
get :health_summary, controller: 'longitudinal_health'
get :health_scores, controller: 'longitudinal_health'
end
# Clinical coding routes
resources :clinical_codes, only: [:index, :show] do
collection do
get :search
get :suggest
end
end
# AI transcription routes
namespace :ai_transcription do
post :start_session
post :process_audio
get :live_updates
post :end_session
end
# Medication continuity routes
resources :encounters do
get :chronic_medications
post :continue_medications
end
end
endError Handling
Standard Error Responses
# LHR-specific error handling
class LhrErrorHandler
def self.handle_lhr_error(error, context = {})
case error
when LhrEnhancementError
{
error: 'lhr_enhancement_failed',
message: 'LHR enhancement failed but core functionality preserved',
details: error.message,
fallback_available: true
}
when ClinicalCodingError
{
error: 'clinical_coding_unavailable',
message: 'Clinical coding service unavailable, manual coding required',
fallback_mode: true
}
when AiTranscriptionError
{
error: 'ai_transcription_failed',
message: 'AI transcription unavailable, manual documentation required',
manual_fallback: true
}
else
{
error: 'lhr_service_error',
message: 'LHR service temporarily unavailable'
}
end
end
endAPI Versioning Strategy
# Support for API versioning as LHR features evolve
module Api
module V1
# Current API with LHR enhancements
class EncountersController < BaseController
# Enhanced implementation
end
end
module V2
# Future API version with additional LHR features
class EncountersController < BaseController
# Future enhancements
end
end
endBenefits of This API Design:
✅ 100% Backward Compatibility: All existing API calls work unchanged
✅ Progressive Enhancement: LHR data appears when available
✅ Optional Features: Clients can opt into LHR features via parameters
✅ Performance Optimized: Heavy LHR processing happens in background
✅ Error Resilient: LHR failures don't break core functionality
✅ Future-Proof: Versioned APIs support evolution of LHR features
Next: See health-scoring.md for detailed health scoring algorithms.