Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / saleforce

Salesforce Apex API Connection to FHIR Server

0.00/5 (No votes)
13 Mar 2023CPOL7 min read 7.2K  
How to make an API connection (GET, POST) using Salesforce Apex language (very like Java) to a FHIR (Electronic Medical Records) Server
I was asked to make an API for Salesforce (SAS) to replace an expensive API Bus software component that is usually used. I used Lightning Web Components for the UI. That uses JavaScript to call Apex (the Salesforce language derived from Java). The Apex code calls the FHIR (Electronic Health Records) API using a library very like RESTSharp. Since it seemed unusual, I am posting the description and working code here. Unfortunately, this needs some background as well because Salesforce is large and seems poorly known. This code is being put here because for all the Salesforce resources out there, code like this wasn't. The learning curve is nasty, but this code is what you get at the end.

Introduction

This provides enough Salesforce background and the code to make GET and POST API calls (to a FHIR Electronic Health Records) server using the Salesforce Apex language (very like Java or C# and this code could very easily be modified to work in Java or C#, some with no modification.)

Salesforce is currently a Web Component based SAS solution that is a popular Sales and CRM solution out of the box. It is meant to be very extensible though. My evaluation at this point is that it is robust and a good product. It looks like they just grabbed all the best technologies they could find and put them all together. (I recognize a lot of the parts.) That means you can do a lot with it, but to avoid aggravation, try to figure out the Salesforce way to do things, what they used (lots of CSV).

I'll try to add introductory information because Salesforce seems pretty novel in the US. There is an incredible amount of learning resources available from Salesforce, from YouTube and other web sources. The eco system is very very developer oriented with free development areas and it comes with robust development tools including extensions for Visual Studio Code (which means refactoring is no fun, so pick variable names very carefully). FYI, I'm a .NET specialist but was hired on a contract because they could not find Salesforce developers. It may be more popular in India and much of the YouTube resources are made by Indians, smart folks them. Salesforce is an impressive system, but I knew nothing about it but its name and that it is big. I'm writing this post to fill in some stuff I had to work hard to learn. I hope it is focused enough to be useful, but my work was pretty broad.

While the project is not about Lightning, the Salesforce Web component implementation, I'll mention it because of its importance. It is the UI and being web components, it is very versatile with AJAX built in. It's a steep learning curve due to its robustness. It seems to be web components (heavy JavaScript) with something like bootstrap, jquery, jquery validation and others all under the hood. It reminds me of ASP.NET 2.0 Web Parts. As such, it is powerful but a tough learning curve.

Learn how to use the Salesforce Developers Console before you try to get very far at all. If you are looking for this code, you probably already know about that.

First, the UI in Lightning. Lightning is huge and I didn't learn much of it really. I did use ChatGPT to make some of it. There is enough Lightning code here to call and return the API values. 'Nuf said. Tons of YouTube Playlists about Lightning.

APIs - Start all API projects with Postman or something like it. There are many test APIs out there and many are free (of which many fail to work), but they tend to go through one ... aggregator that does request a credit card. StackExchange has a useful API you can test with for free.

FHIR Fast Healthcare Interoperability Resources (the API that this code is about) - Electronic Health Records. Different record types are called "resources". These might be Patient resources. They might be Observation resources (all encounters with patients). They might be others, but each represents a JSON record type. The HL7 Specification lists all the different resource types and data types/requirements for them. Probably worth knowing about for anyone doing health care software and great for testing API code. http://hl7.org/fhir/index.html is the HL7 standard. Now it's slick, but slicker still are the YouTube videos playlist by Sidharth Ramesh, an expert on FHIR and Electronic Medical Records in general. It is an amazing resource for learning about all Electronic Health Records and using FHIR with Postman.

Using the Code

Finally, some code... but this is not the code you want. (developer mind trick)... By the way, my code tends to be messy and full of notes about what works, what failed and the gotchas. I've been thanked for that on occasion. I've coded a long time and written really complicated stuff. It's supposed to read like a story. It may look different than many coding styles... but I have had great success. Remember, the main cost of software is maintenance. That is what my code is written for.

Salesforce code breaks into two parts. Classes (.cls files) and Lightning Web Components (LWCs). The .cls file has the API code in it. An LWC is a standard type web component and so includes an HTML file, a JavaScript file, a manifest file and an optional CSS file, all with the same files names but with different file extensions ... all in a folder of the same name. (Look at the file names that match the code.) When you use the CLI to make a new LWC, it makes the templates for you. All 4 LWC files will be in the apiFhirPost folder made by the CLI when you make a new LWC object. This article is about the API code, but you will need the Lightning UI code to call it, so here's about the minimum you will need:

apiFhirPost.html

You will notice the Bootstrap appearance...

HTML
<template>
<lightning-card title="Click button for FHIR API POST Test" 
 class="my-card" style="width: 100%;">
    <div class="slds-m-around_medium">
        <lightning-input type="text" label="Enter Text" class="clTextInput" 
         value={inputValue}  onchange={handleInputChange}></lightning-input>
        
        <div class="slds-m-top_medium">
            <lightning-button variant="brand" label="FHIR API POST" 
             onclick={handleButtonClick}></lightning-button>&nbsp;&nbsp;
        </div>
        <div class="slds-m-top_medium">
            <lightning-formatted-text class="clResponse"  value={response}>
            </lightning-formatted-text>
        </div>
    </div>       
</lightning-card>
</template>

apiFhirPost.js

There is no performance penalty to console.log statements and months after going into production, they may save your cookies. Use them wisely.

JavaScript
import { LightningElement, api, wire, track } from 'lwc';
import {getRecord, getFieldValue} from 'lightning/uiRecordApi';

import calloutForStack from 
 '@salesforce/apex/ApiFhirPost.calloutForStack' //importing method of apex class to js

export default class ApiFhirPost extends LightningElement {
    @api recordId;  /* Will have AccountId on Account page. */
    @api sname;     /* Will have Name on Account page. */
    @api semail;    /* Will have email on Account page. */
    @api response; 
    @track inputValue; 
    // There are two ways of doing this. 
    //@wire(getRecord, {recordId: '$recordId', fields:[NAME_FIELD,EMAIL_FIELD]})
    //@wire(getRecord, {recordId: '$recordId', 
    //fields:['Contact.Name','Contact.Email']})
    //@wire(getRecord, {recordId: '$recordId', fields:FIELDS})
    //record; // data and error

    @wire(calloutForStack, { inputString: '$inputValue' }) //calling apex class method
    wiredMethod({ error, data }) {
        if (data) {
            console.log("apex/ApiFhirPost(12) wiredMethod data: " + data  + '...');
            this.response = data;
            console.log("apex/ApiFhirPost(14) wiredMethod responce: " + 
                         this.response  + '...');
        } else if (error) {
            console.error(error);
        }
    }
    
    imAFunction(inptster) {
        console.log('ImAFunction(53.js): ' + inptster + '...'); 
    }

    // This is used because the onChange event is coming from the input and can be read.
    // When the button is read, it is the button causing the event, not the input.
    // The problem is that this also seems to be triggering the 
    // Apex code (wiredMethod). Verified
    handleInputChange(event) { ; 
        // this.inputValue= event.target.value;
        // console.log('apex/ApiFhirPost.handleInputChange(1f.js):' + 
        // this.inputValue + '...');
    }

    handleButtonClick() {
    console.log('apex/ApiFhirPost.handleButtonClick(1a.js)');

    //var x = document.getElementById("textInput2").value; // Cannot use id 
                                              // as a selector. SF modifies them.
    //document.getElementById("demo").innerHTML = x;
    console.log('apex/ApiFhirPost.handleButtonClick(65) 
      Do calloutForStack()'); //: ' + this.inputValue + '...'); // [object promise]
    //var responce = handleButtonClick();
    /////   responce = handleButtonClick(this.inputValue); // old method, 
                                                           // nada comes back
    // Very interesting article about sending parameters to Apex
    // https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/
    // controllers_server_apex_pass_data.htm
    // @wire(handleButtonClick, { responce }); // Syntax
    
    // Get a reference to the input element ... this works...
    const inputEl = this.template.querySelector('lightning-input');
    // Get the value of the input element
    const inputValue1 = inputEl.value;
    // Perform action based on input value
    console.log("ApiFhirPost.handleButtonClick(77) input: " + inputValue1  + '...');
    //this.response = 'You entered: ' + inputValue1;
    
    // Get a reference to the input element by Id... Fails, 
    // but using a class as a selector works.
    // This does not work
    // "The reason is lwc framework changes the id you define in the 
    // HTML template to some unique values and that 
    // is where your defined id becomes irrelevant. 
    // As per the documentation you should not use ID selectors to 
    // find element in the DOM."
    //const myInput = this.template.querySelector('#idTextInput'); // This fails 
    // as expected. Id gets modified by SF
    // If you need to select an element outside of the component's scope, you can use  
    // the standard document.querySelector() method instead...
    //const myInput = document.querySelector('#idTextInput'); // This fails too.
    //console.log("ApiFhirPost.handleButtonClick(51a) inputId: " + 
    //myInput.value); // Output: the value of the input element
    const myInputCl = this.template.querySelector('.clTextInput'); // This works
    console.log("ApiFhirPost.handleButtonClick(78a) inputCl: " + 
                 myInputCl.value); // Output: the value of the input element
    var strOutputter = '';
    //strOutputter = calloutForStack(myInputCl.value); // This is a promise. 
    //That's a problem...
    console.log("ApiFhirPost.handleButtonClick(81) inputCl: " + 
            strOutputter); // Output: the value of the input element
    var apiOutput = '';        // Call Apex method with input value
        calloutForStack({ inputString: this.inputValue })
            .then(result => {
                //console.log("MyDemo.handleButtonClick30) input: " + 
                //this.inputValue  + '...');
                console.log("ApiFhirPost.handleButtonClick(86) input: " + 
                             myInputCl.value  + '...');
                // For now... "System.HttpResponse[Status=OK, StatusCode=200]"
                // value of div ... Seems to have leading and 
                // trailing double quotes - artifact
                this.response = result;
                console.log("ApiFhirPost.handleButtonClick(89) input: " + 
                             result  + '...');
                // Try to remove leading and trailing double quotes 
                // so div value dosn't show as literal. Don't
                // this.response = result.substring(1,result.length-1); 
                //console.log("ApiFhirPost.handleButtonClick(91) input: " + 
                //result  + '...');
                console.log("ApiFhirPost.handleButtonClick(92) input: " + 
                             JSON.stringify(result)  + '...');
                apiOutput = JSON.parse(result);
                console.log("ApiFhirPost.handleButtonClick(94) data: " + 
                             apiOutput.data  + '...');

                // imAFunction(apiOutput); // it would be nice if this worked 
                                           // but it isn't essential.
                console.log("ApiFhirPost.handleButtonClick(97) Enough nonsense...");
            })
            .catch(error => {
                console.error(error);
            }); 
// The returned value is good and being displayed, but it's not reaching here.
            console.log('apex/ApiFhirPost.handleButtonClick
                         (100.js): ' + result + '...'); 
            console.log('apex/ApiFhirPost.handleButtonClick(101.js): ' + 
                         apiOutput + '...'); 
        }
}

apiFhirPost.css

This is an optional file. This code really shouldn't be needed, but it is.

CSS
.my-card {
    display: block;
    border: 0.2rem outset black;
}

apiFhirPost.js-meta.xml

Just part of it, you should know why by now (Versioning in the default.)

XML
    <isExposed>true</isExposed> 
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Apex Code

SendEmails.cls

Freebee - It can be extremely hard to get data or information out of a Salesforce page. You cannot copy and paste from a screen page. The debugger is only so good. There are other problems so sometimes you just have to email it to yourself.

Apex
// Usage
// SendEmails.sendEmail('bgates@microsquish.com', 'Subject 1', 'Body, what a Fox');
public class SendEmails{
    public static void sendEmail(String sSendTo, String sSubject, String sBody)
    {
    //String strTestString = 'For whom the bell tolls.';
    //Messaging.EmailFileAttachment csvAttachment = 
    //                              new Messaging.EmailFileAttachment();
    //Blob csvBlob = blob.valueOf(generatedCSVFile); 
    //String csvName = 'testfile092022c.csv';
    //csvAttachment.setFileName(csvName);
    //csvAttachment.setBody(csvBlob);
    System.debug('SendEmails.sendEmail().........................'); 
    Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
    //String[] toAddresses = new String[]{'mbreeden@lakecountyil.gov'};
    String[] toAddresses = new String[]{sSendTo};
    //String subject = 'Test File testfile092022ac';
    String subject = sSubject;
    email.setSubject(subject);
    email.setToAddresses(toAddresses);
    //email.setPlainTextBody('Do not ask for whom the bell tolls. 
    //It tools for thee.');
    email.setPlainTextBody(sBody);
    //email.setFileAttachments(new Messaging.EmailFileAttachment[]{csvAttachment});
    Messaging.SendEmailResult[] r = Messaging.sendEmail
                                    (new Messaging.SingleEmailMessage[]{email});
    }

public static void sendEmailStringList(String sSendTo, 
                   String sSubject, List<string> lststr)
    {
    System.debug('SendEmails.sendEmailStringList().........................'); 
    Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
    email.setSubject(sSubject);
    email.setToAddresses(new String[]{sSendTo});
    String sBody = string.join(lststr,','); 
    email.setPlainTextBody(sBody);
    Messaging.SendEmailResult[] r = Messaging.sendEmail
              (new Messaging.SingleEmailMessage[]{email});
    }
}

FHIRAPiClasses.cls

These are the classes that are serialized to JSON to be sent to the FHIR server in the REST request as well as desterilized from the server response. You pretty much need these for all APIs you want to use. Excuse the length, but it is what it is or ... you can type it yourself.

Apex
/* 
FHIR - Fast Healthcare Interoperability Resources

This is based on the resources at "http://http://hl7.org/ - FHIR Documentation.
This file is to define FHIR resources for communication 
with the test FHIR server there.

The objects defined here reflect the JSON returned by 
a GET API call to that FHIR server.
Message Returned by Postman from GET call - http://hapi.fhir.org/baseR4/Patient/8127780

(1) One exercise is to instantiate and populate a [Patient] 
resource object to make the JSON message to put in the body of 
a POST call to pass the new [Patient] record to a FHIR server 
(and return the FHIR id assigned to the [Patient]).
A question arises. If you instantiate a [Patient] resource object in Apex, 
is it going to instantiate the member objects in it 
such as the List<nameResource>. Some languages do, some don't.
When deserializing from JSON, the deserialization code has 
to take care of that automatically, but if you 
instantiate a [Patient] object to use for the body of a POST call, do you have
to instantiate those member objects yourself? 
The Development Console should answer that 
question. The following code will only work if nameResource.given is instantiated as a
List upon instantiation of nameResource

FHIRAPiClasses.nameResource name = new FHIRAPiClasses.nameResource();
name.use = '';
name.family = 'Zagwap';
name.given = new List<String>(); // This is required...
name.given.Add('Freddy')
System.debug('Name: ' + name.family + '...'); 
... Null Pointer Exception... It does not instantiate them... 
and must be done manually in the class code.

(2) When making the POST call, it looked in the log like 
the body might be getting truncated by JSON.serialize() 
so add the code to manually serialize. It's that or the error message:
"diagnostics": "HAPI-0446: Incorrect Content-Type header value of 
\"application/x-www-form-urlencoded\" was provided in the request. 
A FHIR Content-Type is required for \"CREATE\" operation"
... Or both... 
Let's add/replace the header Content-Type: application/xml+fhir" -X 
POST -d ... NO, this is JSON.
*/
public with sharing class FHIRAPiClasses {
    public FHIRAPiClasses() {}
    
    public class patientResponseResource {
        public patientResponseResource() {
            this.meta = new metaResource();
            this.text = new textResource();
            this.name = new List<nameResource>();
            this.telecom = new List<telecomResource>();
            this.address = new List<addressResource>();
        }
        public String resourceType {get;set;}
        public String id {get;set;}    
        public metaResource meta {get;set;}
        public textResource text {get;set;}
        public List<nameResource> name {get;set;}        
        public List<telecomResource> telecom {get;set;}        
        public String gender {get;set;}
        public String birthdate {get;set;}
        public List<addressResource> address {get;set;}

        // This is just to make a useful data output for testing purposes.
        public String strNameIdAddress()
        {
            String dname = ''; // like data name to avoid confusion.
            
            if(this.name.size() > 0)
            {
                dname = this.name[0].family + ', '; // make it easy
                if(this.name[0].given.size() > 0)
                    dname = dname + this.name[0].given[0]; 
            }
            String dId = this.id; 
            String dAddress = '';
            if(this.address.size() > 0)
            {
                if(this.address[0].line.size() > 0)
                { 
                    
                    for (string sline : this.address[0].line)
                        dAddress += sline + ', ';   
                }
                dAddress +=  this.address[0].city + ', ';   
                dAddress +=  this.address[0].state;   
            }
            return dname + ' ID:' + dId + ' - ' + dAddress;
        }
    } // End public class patientResponseResource {
    
        public class metaResource {
            public metaResource()
            {
                this.versionId = '';
                this.lastUpdated = '';
                this.source = '';               
            }
            public metaResource(String VersionId, String LastUpdated, String Source)
            {
                this.versionId = VersionId;
                this.lastUpdated = LastUpdated;
                this.source = Source;
            }
            public String versionId {get;set;}
            public String lastUpdated {get;set;}
            public String source {get;set;}
        }        
    
        public class textResource {
            public textResource() 
            {
                this.status = '';
                this.div = '';
            }
                public textResource(String Status, String Div) 
            {
                this.status = Status;
                this.div = Div;
            }
            public String status {get;set;}
            public String div {get;set;}
        }    
            
        // FHIRAPiClasses.nameResource name = 
        // new FHIRAPiClasses.nameResource('official','Byrne','Billy');
        public class nameResource {
            public nameResource() { given = new List<String>(); }
            public nameResource(String Use, String Family, String Given) 
            { this.given = new List<String>(); 
              this.use = Use;
              this.family = Family;
              this.given.Add(Given);
            }
            public void addNameGiven(String input) {this.given.Add(input);}
            public String use {get;set;}
            public String family {get;set;}
            public List<String> given {get;set;}
        }    
            
        // The JSON from FHIR contains a reserved word "system. 
        // In the Apex code, it needs to do a string replace on the json string 
        // likejsonString.replace('"system":', '"system_x":');
        // Otherwise, you will need to use the JSON.createGenerator() method 
        // to build the JSON manually 
        // (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/
        // apexcode/apex_class_System_Json.htm#apex_System_Json_createGenerator). 
        // This will be incredibly more time consuming than using the 
        // standard serialization.
        public class telecomResource {
            public telecomResource(){}
            public telecomResource(String System_x, String Value, String Use)
            {
                this.system_x = System_x;
                this.value = Value;
                this.use = Use;
            }
            //public String system {get;set;} // "system" "System" are reserved words
            public String system_x {get;set;} 
            public String value {get;set;}
            public String use {get;set;}
        }
    
        public class addressResource {
            public addressResource() 
            {
                 line = new List<String>(); 
                 // So it doesn't send nulls
                 this.city = '';
                 this.state = '';
                 this.PostalCode = '';
            } 
            public addressResource(String Line, String City, 
                                   String State, String postalCode) 
            { this.line = new List<String>();
              this.line.Add(Line);
              this.city = City;
              this.state = State;
              this.PostalCode = postalCode;
            } 

            public void addAddressLine(String input) {this.line.Add(input);}
            public List<String> line {get;set;}
            public String city {get;set;}
            public String state {get;set;}
            public String PostalCode {get;set;}
        }
/*
This seems to be the standard error message format from FHIR. 
Got it for different things.
{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"https://gcc02.safelinks.protection.outlook.com/
     ?url=http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%2F&data=05%7C01%7
     Cmbreeden%40lakecountyil.gov%7C803972ae963443e090ff08db1e4d611
     7%7Cdd536cf592fd42ffa754e98666cb7a96%7C0%7C0%7C638137089921009
     656%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMz
     IiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000%7C%7C%7C&sdata=lA9CQDY
     kRynsC4IhDE46sBiDXXt40%2BfPAxI8Ei0H0%2Fk%3D&reserved=0">
     <h1>Operation Outcome</h1><table border=\"0\"><tr>
     <td style=\"font-weight: bold;\">
     ERROR</td><td>[]</td><td>HAPI-0446: Incorrect Content-Type header value of 
     &quot;application/x-www-form-urlencoded&quot; was provided in the request. 
     A FHIR Content-Type is required for &quot;CREATE&quot; operation</td></tr></table>
     </div>"
  },
  "issue": [ {
    "severity": "error",
    "code": "processing",
    "diagnostics": "HAPI-0446: Incorrect Content-Type header value of 
     \"application/x-www-form-urlencoded\" was provided in the request. 
     A FHIR Content-Type is required for \"CREATE\" operation"
  } ]
}
*/      
        public class errorResource {
            public String resourceType {get;set;}
            public textResource text {get;set;}
            public List<issueResource> issue {get;set;}

            public errorResource()
            {
                this.resourceType = '';
                this.text = new textResource();
                this.issue = new List<issueResource>();     
            }
        }

        public class issueResource {
            public String severity {get;set;}
            public String code {get;set;}
            public String diagnostics {get;set;}
 
            public issueResource()
            {
                this.severity = '';
                this.code = '';
                this.diagnostics = '';
            }
        }
} // End public with sharing class FHIRAPiClasses

ApiResponseClass1.cls

This is for transferring processed messages from API code to the Apex
class that consumes it. The main purpose and use of this is to be able to have an error and
data section. The Response is going to the JavaScript function of the LWC so it needs to
be string of JSON. ... Since it's returning a string, not JSON, is this needed? You can't
return an Apex object to JavaScript, so this might or might not be needed. ... Figure it
will be needed at some point.

Yeah, this code sucks, but it works.

Apex
public with sharing class ApiResponseClass1 {
    // statusCode and status in one string
    @AuraEnabled  public String sCompleteStatus{ get; set; }
    // Http Status Code
    @AuraEnabled  public String sStatusCode{ get; set; }
    @AuraEnabled  public String sdata{ get; set; }
    @AuraEnabled  public String serror{ get; set; }
  
    public ApiResponseClass1() { } // Default constructor
    
    // Trivial constructor, for server-side Apex -> client-side JavaScript
    // ... Really cannot be used as API code is implemented
    public ApiResponseClass1(String psStatusCode, String psdata, String pserror) { 
        this.sStatusCode = psStatusCode;
        this.sdata = psdata;
        this.serror = pserror;
    } // constructor
    
    public void setValues(String psStatusCode, String psdata, String pserror) { 
        this.sStatusCode = psStatusCode;
        this.sdata = psdata;
        this.serror = pserror;
    } 

    /*** The rest of this shouldn't be doing anything ***/

    //public String sresponse{ get; set; }
    public String getToJson(){ 
        /* String sresponse = '{"data":"' +
            String.isNotBlank(this.sdata) ? this.sdata : '' +
            '","error":"' +
            String.isNotBlank(this.serror) ? this.serror : '' +
            '"}'; */
   
        String ssStatusCode = '';
        String ssdata = '';
        String sserror = '';
       if(String.isNotBlank(this.sdata) == true)
            ssdata = this.sdata;
       if(String.isNotBlank(this.serror) == true)
            sserror = this.serror;
        if(String.isNotBlank(this.sStatusCode) == true)
            ssStatusCode = this.sStatusCode;

        String sresponse = '{"Data":"' + ssdata + '","Status Code":"' + 
        ssStatusCode + '","CompleteStatus":"' + sCompleteStatus + '", 
        "Error":"' + sserror + '"}';

        return sresponse;
    }
}

apiFhirGet.cls

What a mess... Notice that the second method:

Apex
httpResponse callout(String httpMethod, String endpoint, String body

It is generic to GET, POST, PUT... This code could use touchups, but it works...

Apex
// Ultimately based on Integrating Third-party APIs Using APEX RESTful Callouts
// https://www.youtube.com/watch?v=705SeyjpoFs// For now, this will be called by 
                                              // ApiFhirGet.cls
//

public with sharing class ApiFhirGet {
    public ApiFhirGet() {
    }
    public static String BASE_URL = 'http://hapi.fhir.org/baseR4/';

    public static String tellMeYourName()
    {
        String sMyName = 'My Name is tellMeYourName(3) in ApiFhirGet.';
        system.debug(sMyName);   
        return sMyName;
    }

    @AuraEnabled(cacheable=true)
        public static String calloutForStack(String sInputString) {
        // Do some processing with the input string
        ApiResponseClass1 apiResponse = new ApiResponseClass1();

        String outputString = 'FhirGet Processed string: ' + sInputString;
        String strInput = sInputString; // back to where we were
        String httpMethod = 'GET';
        system.debug('Method ApiFhirGet.calloutForStack(1a) input:' + strInput + '...');

        ///// This seems to be a code killer for some reason. 
        ///// Hardcoded works. Using the input from LWC is a problem 
        ///// and returns a 500 server error.
        ///// It does work with input in the Development Console
        //String endPoint = BASE_URL + 'Patient/8271353'; // For some reason only 
                                                          // this works. 
                                                          // LWC input fails.
        String endPoint = BASE_URL + 'Patient/8301013';   // For some reason only 
                                                          // this works. 
                                                          // LWC input fails.
        //String endPoint = BASE_URL + 'Patient/' + sInputString.trim(); // Problem
        //String endPoint = BASE_URL + 'Patient/' + strInput.trim();     // Problem
        //String endPoint = BASE_URL + strInput;                         // Problem
        system.debug('Method ApiFhirGet.calloutForStack(1ep) endpoint: ' + 
                      endPoint + '...');

        String strReponseBody = '';
        HttpResponse res = callout('GET', endPoint, '');
        System.debug('calloutForStack(1ab)..'); 

        //System.debug('calloutForStack GetBody(1s): ' + strReponseBody + '...'); 
        
        // Response values from code shown in youtube.com/watch?v705SeyjppFs
        Integer statusCode=res.getStatusCode();     // ? success or error
        System.debug('calloutForStack(1d) statusCode: ' + statusCode + '...');  
        Integer iStatus= res.getStatusCode();       // 
        String sStatus= iStatus.format();           // 
        apiResponse.sStatusCode = iStatus.format(); //
        // statusCode and status in one string
        // In any case, this will be added to the error if there is one.        
        //String strEntireStatus = res.toString();
        apiResponse.sCompleteStatus = res.toString();

        String sResponse = '';
    
        // Looks like 'Status=OK, StatusCode=200'
        if(iStatus == 200){
            System.debug('calloutForStack(1em) entireStatus: ' + 
                          apiResponse.sCompleteStatus + '...');
    
            // The JSON from FHIR contains a reserved word "system. 
            // In the Apex code, it needs to do a string replace 
            // on the json string likejsonString.replace('"system":', '"system_x":');
            strReponseBody = res.getBody(); // This is everything received.
            strReponseBody = strReponseBody.replace('"system":', '"system_x":');
            System.debug('calloutForStack(1ea) body: ' + strReponseBody + '...');  
            
            List<String> headerKeys = res.getHeaderkeys();
            System.debug('calloutForStack(1fa) Header Size: ' + 
                          String.valueOf(headerKeys.size()) + '...');  
            // Works good. It shows 14 headers, so to display them
            for (String key : headerKeys) 
            {
                // Get a specific header value by key
                String svalue = res.getHeader(key); 
                System.debug('calloutForStack(1fb) Header key=' + 
                              key + '- value=' + svalue + '...');
            } 
            
            // Working with JSON in Apex: A Primer - 
            // Shows how to deserialize an unknown JSON string
            // https://ktema.org/articles/json-apex-intro/            
            // https://developer.salesforce.com/docs/
            // atlas.en-us.apexref.meta/apexref/apex_methods_system_map.htm
            Map<String, Object> result = 
                (Map<String, Object>)JSON.deserializeUntyped(strReponseBody);
              
            // A set is an unordered collection of elements that do not contain 
            // any duplicates. 
            // Set elements can be of any data type—primitive types, 
            // collections, sObjects, user-defined types, and built-in Apex types.
            Set <String> resSet = new Set<String>();
            resSet = result.keySet(); // Should be the keys for all objects            
            List<String> lststr = new List<String>(resSet);
            String sq = lststr[0];
            System.debug('calloutForStack(1hp) Key: ' + 
                          sq + '...'); // returns 'items' ... makes sense
            //system.debug('calloutForStack(1j): ' + Result + '...');

            sResponse = '{"Status":"' + sStatus + '"}';
            System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');

            // This needs replacing. It's probably the best to use.
            FHIRAPiClasses.patientResponseResource RR = 
                   (FHIRAPiClasses.patientResponseResource)JSON.deserialize
                   (res.getBody(),FHIRAPiClasses.patientResponseResource.class);
            String ssResponse = RR.text.div.trim();
            string sDiv = '<div xmlns="http://www.w3.org/1999/xhtml">';
            sResponse = ssResponse.replace(sDiv, '<div>');
            // It still displays this as a literal, 
            // so use different data from the structure

            // Parse response to structure here if needed   

            //strData = RR.strNameIdAddress(); 
            apiResponse.sdata = RR.strNameIdAddress();
            system.debug('calloutForStack(112): ' +  apiResponse.sdata + '...');          

        } // End if(iStatus == 200)
        else { // There was an error
            apiResponse.serror = '(2)Unknown Error GETing FHIR data'; 

            // This should parse the returned error. See ApiFhirPost.cls
        }

        // When an instance of an Apex class is returned from a server-side action, 
        // the instance is serialized to JSON by the framework. 
        // Only the values of public instance properties and 
        // methods annotated with @AuraEnabled are serialized and returned. 
        // getToJson(); is so annotated.
        //return apiResponse.getToJson();    // This does return JSON as expected, 
                                             // but the method makes it. 
        //return new ApiResponseClass1(apiResponse.sCompleteStatus, 
        //strReponseBody, strError); // oops
        //system.debug('calloutForStack(1p): Status=' +  apiResponse.sdata);
        //return new ApiResponseClass1(apiResponse.sCompleteStatus, 
        //strData, strError); // complains
        // This is the token that will be returned. @AuraEnabled allows 
        //for automatic serialization        
        //ApiResponseClass1 apiResponse = new ApiResponseClass1
        //(apiResponse.sCompleteStatus, strData, strError);
        //return apiResponse;

        sResponse = apiResponse.getToJson();

        system.debug('Method ApiFhirGet.calloutForStack(9za) sResponse:' + 
                      sResponse + '...');
        //return outputString;
        //return apiResponse.sCompleteStatus; // works - 
        //"System.HttpResponse[Status=OK, StatusCode=200]"...
        return sResponse; 
         //return strInput;
    }    

    // Method to perform a callout and return an httpResponse
    // Notice that here, this call is generic because it takes any URL
    // Sample for Development Console:  ApiFhirGet.callout
    // ('GET','https://v2.jokeapi.dev/joke/Any',''); 
    // Warning: Has some nasty jokes unless told to be clean
    public static httpResponse callout
           (String httpMethod, String endpoint, String body){
        //Instantiate an httpRequest and set the required attributes
        httpRequest req = new httpRequest();
        req.setMethod(httpMethod);
        req.setEndpoint(endpoint);

        if(string.isNotBlank(body))    {
            req.setBody(body);
            req.setHeader('Content-Length', string.valueOf(body.Length()));
        }

        //Optional attributes are often required to conform to the 
        //3rd Party Web Service Requirements
        req.setHeader('Accept-Encoding','gzip, deflate');
        // Might be needed for FHIR or else returns just html
        req.setHeader('Content-Type','application/json');
        // Absolutely needed for FHIR or else returns just html
        req.setHeader('Accept','application/json'); 

        //You can adjust the timeout duration (in milliseconds) 
        //to deal with slow servers or large payloads
        req.setTimeout(120000);

        req.setCompressed(true);

        //Use the HTTP Class to send the httpRequest and receive an httpResposne
        /*If you are not using an HttpCalloutMock: 
        if (!test.isRunningTest){
        */
        httpResponse res = new http().send(req);
        /*If you are not using an HttpCalloutMock: 
        }
        */
        system.debug(res.toString());
        system.debug(res.getBody());

        // Response values from code shown in youtube.com/watch?v705SeyjppFs
        // This is basically sample code because they can't be used for anything here.
        /*
        Integer statusCode=res.getStatusCode(); // ? success or error
        String status= res.getStatusCode();     // error name
        // statusCode and status in one string
        String entireStatus = res.toString();
        // Looks like 'Status=OK, StatusCode=200'
        String body = res.getBody();
        List<String> headerKeys = res.getHeaderkeys();
        // Get a specific header value by key
        String headerX = res.getHeader(KEY); 
        */

        return res;
    }

apiFhirPost.cls

Again, notice that the second method could be in its own class... probably... and used by all REST calls.

Apex
// For now, this will be called by ApiFhirPost.cls
//

public with sharing class ApiFhirPost {
    public ApiFhirPost() {
    }
    public static String BASE_URL = 'http://hapi.fhir.org/baseR4/';

    public static String tellMeYourName()
    {
        String sMyName = 'My Name is tellMeYourName(3) in ApiFhirPost.';
        system.debug(sMyName);   
        return sMyName;
    }

    // POSTed Patient Id's: 8127780, 8186215,
    // Seaton ID = 8271353  DuQuesne ID = 8271558

    @AuraEnabled(cacheable=true)
    // Eventually sInputString would be the Account.id and 
    // all the data to POST would come from that through SOQL
    public static String calloutForStack(String sInputString) {
        ApiResponseClass1 apiResponse = new ApiResponseClass1();
        // Do some processing with the input string
        String outputString = 'FhirGet Processed string: ' + sInputString;
        String strInput = sInputString; // back to where we were
        String httpMethod = 'POST';
        system.debug('Method ApiFhirPost.calloutForStack(1a) input:' + 
                      strInput + '...');

        String endPoint = BASE_URL + 'Patient'; // from postman
        //String endPoint = BASE_URL + 'Patient/8271558'; // To force an error
     
        system.debug('Method ApiFhirPost.calloutForStack(1ep) endpoint:' + 
                      endPoint + '...');
        
        String strRequestBody = '';
        FHIRAPiClasses.patientResponseResource patient = 
                       new FHIRAPiClasses.patientResponseResource();
        System.debug('calloutForStack(48jab): ' + strRequestBody + '...'); 

        //public String resourceType {get;set;}
        patient.resourceType = 'Patient';
        //public String id {get;set;} - Set by FHIR Server and comes back in body
        patient.id = '';
        //public metaResource meta {get;set;} // not sent

        //public textResource text {get;set;} // not sent

        //public List<nameResource> name {get;set;}        
        patient.name.Add(new FHIRAPiClasses.nameResource('official','Woo','David'));

        //public List<telecomResource> telecom {get;set;}
        // public telecomResource(String System_x, String Value, String Use)
        patient.telecom.Add(new FHIRAPiClasses.telecomResource
                           ('phone', '8012121756', 'mobile'));
        patient.telecom.Add(new FHIRAPiClasses.telecomResource
                           ('email', 'dwoo@lmicrosquish.com', ''));

        //public String gender {get;set;}
        patient.gender = 'male';
        //public String birthdate {get;set;}
        patient.birthdate = '2508-06-03';
        //public List<addressResource> address {get;set;}
        //public addressResource(String Line, String City, String State, "PostalCode") 
        patient.address.Add(new FHIRAPiClasses.addressResource
                ('200 CCatskinner Dr.', 'Slash City', 'TY', '54545'));

        // https://ktema.org/articles/json-apex-intro/ ... primer...
        strRequestBody = JSON.serialize(patient); // This call may produce 
                                                  // truncated the JSON...
        System.debug('calloutForStack(1jab): ' + strRequestBody + '...'); 

        // Sending an email here causes a CalloutExcpeption from the POST request
        // This is an error I got, but it was because I tried to send an email 
        // before I sent the RST POST Call.
// System.CalloutException: You have uncommitted work pending. 
// Please commit or rollback before calling out
// 1. Salesforce enforces a limit of one API call (i.e., callout) per Apex routine. 
// This is the Salesforce 
// error caused by making two or more API calls in the same Apex routine.
// 2. One of the framework requirements when developing on the Salesforce 
// platform is that you cannot 
// perform any DML prior to making a callout to any external service 
// in the same transaction.     
        //SendEmails.sendEmail('bgates@microsquish.com', 
        //'Subject Post data 1', strRequestBody);
        System.debug('calloutForStack(1jac)..'); 
        
        String strReponseBody = '';
        String strData = ''; // What is actually going to be returned.
        //return callout(httpMethod, endpoint, body);
        HttpResponse res = callout('POST', endPoint, strRequestBody);
        String strRawReponse = res.getBody();
        System.debug('calloutForStack(1ab)...' + strRawReponse); 

        // Now I've got an error here... hopefully sending it as an email 
        // after the Callout will not cause an exception.
        // Nah, it never gets here... In the Debug log, it looks like 
        // the serialized JSON was truncated. (Not in POAS message though.)
        // I may need to do it manually instead of using JSON.Serialize(object) - Nope
        //SendEmails.sendEmail('bgates@microsquish.com', 
        //'Subject Post data 1', strRawReponse); 

        Integer iStatus= res.getStatusCode(); // Response Code
        apiResponse.sStatusCode = iStatus.format(); //
        System.debug('calloutForStack(1d) statusCode: ' + 
                      apiResponse.sStatusCode + '...');  

        // statusCode and status in one string
        // Looks something like "System.HttpResponse[Status=Created, StatusCode=201]"
        // In any case, this will be added to the error if there is one.        
        apiResponse.sCompleteStatus = res.toString();
        String sResponse = '';

        if(iStatus == 201){
            System.debug('calloutForStack(1em) entireStatus: ' + 
                          apiResponse.sCompleteStatus + '...');
    
            // The JSON from FHIR contains a reserved word "system. 
            // So in the Apex code it needs to do a string replace on the json string 
            // likejsonString.replace('"system":', '"system_x":');
            strReponseBody = res.getBody(); // This is everything received.
            strReponseBody = strReponseBody.replace('"system":', '"system_x":');
            System.debug('calloutForStack(1ea) body: ' + strReponseBody + '...');  
            
            List<String> headerKeys = res.getHeaderkeys(); // Not really needed, but...
            System.debug('calloutForStack(1fa) Header Size: ' + 
                          String.valueOf(headerKeys.size()) + '...');  
            // Works good. It shows 14 headers, so to display them
            for (String key : headerKeys) 
            {
                // Get a specific header value by key
                String svalue = res.getHeader(key); 
                System.debug('calloutForStack(1fb) Header key=' + 
                              key + '- value=' + svalue + '...');
            } 
            
            // Working with JSON in Apex: A Primer - Shows how to deserialize 
            // an unknown JSON string.
            // This is completely unnecessary here because below the response is 
            // deserialized to a known object type.
            // https://ktema.org/articles/json-apex-intro/            
            // https://developer.salesforce.com/docs/
            // atlas.en-us.apexref.meta/apexref/apex_methods_system_map.htm
            Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped
                                         (strReponseBody);
              
            // A set is an unordered collection of elements that do not 
            // contain any duplicates. 
            // Set elements can be of any data type—primitive types, 
            // collections, sObjects, 
            // user-defined types, and built-in Apex types.
            ////Set <String> resSet = new Set<String>();
            ////resSet = result.keySet(); // Should be the keys for all objects            
            ////List<String> lststr = new List<String>(resSet);
            ////String sq = lststr[0];
            ////System.debug('calloutForStack(1hp) 
            ////Key: ' + sq + '...'); // returns 'items' ... makes sense
            //system.debug('calloutForStack(1j): ' + Result + '...');

            sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
            System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');

            // This needs replacing. It's probably the best to use.
            FHIRAPiClasses.patientResponseResource RR = 
              (FHIRAPiClasses.patientResponseResource)JSON.deserialize
              (res.getBody(),FHIRAPiClasses.patientResponseResource.class);
            String ssResponse = RR.text.div.trim();
            string sDiv = '<div xmlns="http://www.w3.org/1999/xhtml">';
            sResponse = ssResponse.replace(sDiv, '<div>');
            // It still displays this as a literal, so use different data 
            // from the structure

            strData = RR.strNameIdAddress(); // Not all data in the response.
            apiResponse.sdata = RR.strNameIdAddress();  // Not all data in the response.
            system.debug('calloutForStack(112): ' +  strData + '...');          

        } // End if(iStatus == 201)

        else if (iStatus == 400) { // FHIR error
            System.debug('calloutForStack(12em) entireStatus: ' + 
                          apiResponse.sCompleteStatus + '...');
    
            sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
            System.debug('calloutForStack(12epm) entireStatus: ' + sResponse + '...');

            // The JSON from FHIR contains a reserved word "system. 
            // In the Apex code, it needs to do a string replace 
            // on the json string likejsonString.replace('"system":', '"system_x":');
            strReponseBody = res.getBody(); // This is everything received.
            
            System.debug('calloutForStack(12ea) body: ' + strReponseBody + '...');  
            
            FHIRAPiClasses.errorResource RR1 = 
            (FHIRAPiClasses.errorResource)JSON.deserialize(res.getBody(),
             FHIRAPiClasses.errorResource.class);
            
            if(RR1.issue.size() > 0)
                apiResponse.serror = RR1.issue[0].diagnostics;
            else 
                  apiResponse.serror = '(1)Unknown Error POSTing FHIR data'; 
        }
        else { // There was an error
            apiResponse.serror = '(2)Unknown Error POSTing FHIR data'; 
        }

        // When an instance of an Apex class is returned from a server-side action, 
        // the instance is serialized to JSON by the framework. 
        // Only the values of public instance properties and 
        // methods annotated with @AuraEnabled are serialized and returned. 
        // getToJson(); is so annotated.
        system.debug('calloutForStack(1p): Status=' + apiResponse.getToJson());
        //return new ApiResponseClass1(apiResponse.sCompleteStatus, 
        // apiResponse.sdata , strError); // complains
        // This is the token that will be returned. @AuraEnabled allows for 
        // automatic serialization        
        //apiResponse.setValues(apiResponse.sCompleteStatus, 
        //apiResponse.sdata , strError);
        //return apiResponse; // mAybe a good idea if needed

        sResponse = apiResponse.getToJson();

        system.debug('Method ApiFhirPost.calloutForStack(9za) 
                      sResponse:' + sResponse + '...');
       
        return sResponse; 
    }    

    // Method to perform a callout and return an httpResponse
    // Notice that here, this call is generic because it takes any URL
    // Sample for Development Console:  ApiFhirPost.callout
    // ('GET','https://v2.jokeapi.dev/joke/Any',''); 
    // Warning: Has some really nasty jokes unless told to be clean
    public static httpResponse callout
           (String httpMethod, String endpoint, String body){
        //Instantiate an httpRequest and set the required attributes
        httpRequest req = new httpRequest();
        req.setMethod(httpMethod);
        req.setEndpoint(endpoint);

        if(string.isNotBlank(body))    {
            req.setBody(body);
            req.setHeader('Content-Length', string.valueOf(body.Length()));
        }

        //Optional attributes are often required to conform to the 
        //3rd Party Web Service Requirements
        req.setHeader('Accept-Encoding','gzip, deflate');
        // Might be needed for FHIR GET or else returns just html
        req.setHeader('Content-Type','application/json');
        // Absolutely needed for FHIR GET or else returns just html
        req.setHeader('Accept','application/json'); 
        // At one point got error message that it needed header header 
        // like this for a create:
        //?req.setHeader('Content-Type: application/json+fhir" -X POST -d'); 

        //You can adjust the timeout duration (in milliseconds) 
        //to deal with slow servers or large payloads
        req.setTimeout(120000);

        req.setCompressed(true);

        //Use the HTTP Class to send the httpRequest and receive an httpResposne
        /*If you are not using an HttpCalloutMock: 
        if (!test.isRunningTest){
        */
        httpResponse res = new http().send(req);
        /*If you are not using an HttpCalloutMock: 
        }
        */
        system.debug(res.toString());
        system.debug(res.getBody());

        // Response values from code shown in youtube.com/watch?v705SeyjppFs
        // This is basically sample code because they can't be used for anything here.
        /*
        Integer statusCode=res.getStatusCode(); // ? success or error
        String status= res.getStatusCode(); // error name
        // statusCode and status in one string
        String entireStatus = res.toString();
        // Looks like 'Status=OK, StatusCode=200'
        String body = res.getBody();
        List<String> headerKeys = res.getHeaderkeys();
        // Get a specific header value by key
        String headerX = res.getHeader(KEY); 
        */

        return res;
    }

    //Method to deserialize the response body
    //public static FHIRAPiClasses.patientResponseResource deserialize
    //(httpResponse res){
    //    return (FHIRAPiClasses.patientResponseResource)JSON.deserialize
    //    (res.getBody(),FHIRAPiClasses.patientResponseResource.class);
    //}


} // End public with sharing class ApiFhirPost 

What a hassle...

Points of Interest

Keep in mind to put visual tags in your UI code because there may be a deployment lag. You need to know if you are looking at the code you just deployed. You may not be. Class code seems to deploy faster.

I dunno, Salesforce is interesting but thinking of how long it took me to learn standard web development in an ASP environment (Classic, Framework, Core), I did my time. Salesforce, Lightning, Apex is pretty impressive though. If you are early to mid in your career, I would consider learning it. It inherently has good security and few seem to know it. Very slick system.

The next step would be to expand this to the DocuSign API, but that is its own ecology and it is ROBUST!!! Still, this is the basic code you would need with the DocuSign data structures.

I hope this code helps someone. I could have used it. Also, with only minor modifications, this is great code for a Java or C# API.

History

  • 13th March, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)