DEV Community

Cover image for Setting Up a Managed FHIR Server on AWS Using AWS HealthLake
Paul Aderoju
Paul Aderoju

Posted on

Setting Up a Managed FHIR Server on AWS Using AWS HealthLake

Introduction

Setting up a FHIR server from scratch is no small feat. The traditional route means provisioning backend infrastructure, configuring a database, building a CI/CD pipeline, managing server patches, and locking down networking with subnets, security groups, and ACLs — and that's before you've written a single line of application code.

AWS HealthLake cuts through all of that. It's a fully managed, HIPAA-eligible service that gives you a production-grade FHIR R4 repository without the operational overhead. In this guide, you'll learn how to spin one up, connect to it, and start working with real patient data using Postman — then see how to wire it into a live application.

Why HealthLake?

  • Fully managed FHIR R4 infrastructure — no servers to patch or scale
  • HIPAA-eligible with built-in encryption and access controls
  • Sub-millisecond latency at petabyte scale
  • SMART on FHIR support for standards-based authentication and authorization
  • Automatic export to Apache Iceberg for SQL-on-FHIR analytics
  • Built-in NLP to extract clinical context from unstructured medical text

Part 1: Provisioning Your HealthLake Data Store

Step 1 — Navigate to AWS HealthLake

Log in to your AWS Management Console and use the search bar at the top to search for HealthLake. Click on the AWS HealthLake result to open the service dashboard.

HealthLake Console

Step 2 — Open the Data Stores Panel

On the HealthLake landing page, click View Data Stores (or Create Data Store if you're starting fresh). This is your central hub for managing FHIR repositories.

Step 3 — Create a New Data Store

Click Create Data Store and fill in the configuration:

  • Data Store Name: Give it a meaningful name, e.g. my-fhir-app
  • Format: Select FHIR R4. This is the data schema all your FHIR resources will conform to.
  • Preload Sample Data (Optional): If you'd like to explore the service with synthetic patient data right away, enable this. The data is generated by Synthea — a realistic but fictional patient population. Note: enabling this adds 10–20 minutes to initialization time.

Step 4 — Configure Encryption

Under Data Store encryption, you have two options:

  • AWS Owned Key (Default): AWS manages the key on your behalf. This is the quickest option and appropriate for most use cases.
  • Customer Managed Key (Advanced): Bring your own AWS KMS key if your compliance requirements demand it. This gives you full key lifecycle control.

Tip: For most development and production workloads, the AWS-owned key is sufficient. Use a customer-managed key only if your security policy requires explicit key rotation or audit trails.

Create a HealthLake Store

Step 5 — Add Tags (Optional)

Tags are key-value metadata labels, useful for cost allocation, access policies, and resource organization — for example Environment: production or Team: clinical-apps. Skip this for quick experiments.

Step 6 — Create and Wait

Click Create Data Store. Provisioning typically takes between 10 and 30 minutes. The status will show as Creating during this time.

Note: HealthLake is spinning up dedicated infrastructure for your FHIR repository. You can stay on the Data Stores page and refresh periodically to check progress.

Step 7 — Copy Your Endpoint

Once the status changes to Active, open the data store details. You'll see:

  • Data Store ID
  • ARN (Amazon Resource Name)
  • Endpoint — this is the FHIR base URL you'll use for all API calls

Copy the endpoint. It will look something like this:

https://healthlake.us-east-1.amazonaws.com/datastore/<YOUR_DATASTORE_ID>/r4/
Enter fullscreen mode Exit fullscreen mode

Healthlake store dashboard


Part 2: Testing Your FHIR Endpoint with Postman

Before wiring HealthLake into your application, it's good practice to validate the endpoint manually. Postman is ideal for this — it supports AWS Signature V4 authentication out of the box, which HealthLake requires.

Authentication: AWS Signature V4

HealthLake uses IAM-based authentication. Every request must be signed using AWS Signature V4. In Postman, this is handled automatically once configured.

Open the Authorization tab in your Postman request and set the following:

Field Value
Auth Type AWS Signature
AccessKey Your IAM Access Key ID
SecretKey Your IAM Secret Access Key
AWS Region us-east-1 (or your deployment region)
Service Name healthlake

AWS Signature on Postman

IAM: Make sure the IAM identity you're using has the AmazonHealthLakeFullAccess policy attached, or at minimum the healthlake:CreateResource and healthlake:ReadResource actions scoped to your data store's ARN.

Creating a Patient Resource (POST)

Set up your Postman request as follows:

  • Method: POST
  • URL: https://healthlake.us-east-1.amazonaws.com/datastore/<YOUR_DATASTORE_ID>/r4/Patient
  • Headers: Content-Type: application/json
  • Body: Raw JSON
{
  "resourceType": "Patient",
  "id": "example-patient-1",
  "name": [{
    "use": "official",
    "family": "Doe",
    "given": ["John"]
  }],
  "gender": "male",
  "birthDate": "1990-05-15",
  "telecom": [{
    "system": "phone",
    "value": "+2348012345678",
    "use": "mobile"
  }],
  "address": [{
    "city": "Lagos",
    "country": "Nigeria"
  }]
}
Enter fullscreen mode Exit fullscreen mode

Hit Send. A successful response returns HTTP 201 Created with the full resource body, including a server-generated UUID as the resource ID and a meta.lastUpdated timestamp — confirming your record was persisted.

Postman HealthLake Patient GET request

Reading a Patient Resource (GET)

To retrieve the patient you just created, use the resource ID from the 201 response:

GET .../r4/Patient/<RESOURCE_ID>
Enter fullscreen mode Exit fullscreen mode

You can also search across all patients using query parameters:

# Search by family name
GET .../r4/Patient?family=Doe

# Search by birthdate
GET .../r4/Patient?birthdate=1990-05-15

# Get all patients (paginated)
GET .../r4/Patient
Enter fullscreen mode Exit fullscreen mode

Updating a Patient Resource (PUT)

To update an existing patient, use a PUT request with the full updated resource body:

PUT .../r4/Patient/<RESOURCE_ID>
Enter fullscreen mode Exit fullscreen mode

HealthLake performs a full replace — not a partial update. For partial updates, use the PATCH method with a FHIR Patch document.

Deleting a Resource (DELETE)

DELETE .../r4/Patient/<RESOURCE_ID>
Enter fullscreen mode Exit fullscreen mode

Note: HealthLake soft-deletes resources by default — they're marked as deleted but remain in the audit trail. This supports HIPAA compliance requirements around data retention and auditability.


Part 3: Using HealthLake in a Live Application

Now that you've confirmed the endpoint works, let's integrate HealthLake into a real application. We'll use Node.js with the AWS SDK v3, which handles Signature V4 signing automatically.

Setup: Install Dependencies

npm install @aws-sdk/client-healthlake
npm install @aws-sdk/signature-v4
npm install axios
npm install @aws-sdk/credential-provider-node
Enter fullscreen mode Exit fullscreen mode

Initializing the AWS HTTP Client with SigV4 Signing

Rather than using the HealthLake SDK client (which has limited FHIR-specific support), the recommended approach is to sign requests manually using the AWS HTTP signing library, then use any HTTP client to call the endpoint directly.

const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { defaultProvider } = require('@aws-sdk/credential-provider-node');
const { Sha256 } = require('@aws-crypto/sha256-js');
const axios = require('axios');

const REGION = 'us-east-1';
const DATASTORE_ID = process.env.HEALTHLAKE_DATASTORE_ID;
const BASE_URL = `https://healthlake.${REGION}.amazonaws.com/datastore/${DATASTORE_ID}/r4`;

async function signedRequest(method, path, body = null) {
  const credentials = await defaultProvider()();
  const signer = new SignatureV4({
    credentials,
    region: REGION,
    service: 'healthlake',
    sha256: Sha256
  });

  const url = new URL(`${BASE_URL}${path}`);
  const request = {
    method,
    hostname: url.hostname,
    path: url.pathname + url.search,
    headers: {
      host: url.hostname,
      'content-type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined
  };

  const signed = await signer.sign(request);
  return axios({
    method,
    url: url.toString(),
    headers: signed.headers,
    data: body
  });
}
Enter fullscreen mode Exit fullscreen mode

Creating a Patient from Your App

async function createPatient(patientData) {
  const response = await signedRequest('POST', '/Patient', patientData);
  console.log('Created patient:', response.data.id);
  return response.data;
}

// Usage
createPatient({
  resourceType: 'Patient',
  name: [{ use: 'official', family: 'Adeyemi', given: ['Amara'] }],
  gender: 'female',
  birthDate: '1988-03-22',
  address: [{ city: 'Abuja', country: 'Nigeria' }]
});
Enter fullscreen mode Exit fullscreen mode

Searching for Patients

async function searchPatients(params) {
  const query = new URLSearchParams(params).toString();
  const response = await signedRequest('GET', `/Patient?${query}`);
  const bundle = response.data;
  console.log(`Found ${bundle.total} patients`);
  return bundle.entry?.map(e => e.resource) ?? [];
}

// Search by name
const patients = await searchPatients({ family: 'Adeyemi' });
Enter fullscreen mode Exit fullscreen mode

Environment Configuration

Store your configuration in environment variables — never hardcode credentials:

# .env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
HEALTHLAKE_DATASTORE_ID=your_datastore_id
Enter fullscreen mode Exit fullscreen mode

Security: In production, use IAM roles attached to your EC2/ECS/Lambda instance instead of static credentials. This avoids storing secrets entirely and is the AWS-recommended approach.


Production Best Practices

IAM Least Privilege

Create a dedicated IAM role for your application with only the HealthLake permissions it needs. Avoid attaching broad policies like AdministratorAccess.

Error Handling

HealthLake returns standard FHIR OperationOutcome resources on errors. Parse these to surface meaningful messages to your logs:

try {
  const result = await signedRequest('POST', '/Patient', patient);
} catch (err) {
  if (err.response?.data?.resourceType === 'OperationOutcome') {
    const issues = err.response.data.issue;
    issues.forEach(i => console.error(`[${i.severity}] ${i.diagnostics}`));
  }
}
Enter fullscreen mode Exit fullscreen mode

SMART on FHIR for Multi-Tenant Apps

If you're building a patient-facing or clinician-facing application that needs OAuth2-based access control, HealthLake supports SMART on FHIR. This lets you scope access tokens to specific patients, resource types, and operations — a key requirement for EHR integrations.


Wrapping Up

AWS HealthLake removes the infrastructure complexity from FHIR development. Instead of managing servers, databases, and networking, you can focus on building healthcare applications that matter.

Here's a recap of what you've covered:

  • Provisioned a fully managed FHIR R4 data store on AWS HealthLake
  • Configured AWS Signature V4 authentication in Postman
  • Created, read, updated, and deleted FHIR Patient resources via REST
  • Integrated the HealthLake endpoint into a Node.js application
  • Applied security and production best practices

From here, you can explore other FHIR resource types — Observation, Encounter, Condition, MedicationRequest — and start building out a full clinical data platform, all without a single server to manage.


Top comments (0)