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...
<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>
</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.
import { LightningElement, api, wire, track } from 'lwc';
import {getRecord, getFieldValue} from 'lightning/uiRecordApi';
import calloutForStack from
'@salesforce/apex/ApiFhirPost.calloutForStack'
export default class ApiFhirPost extends LightningElement {
@api recordId;
@api sname;
@api semail;
@api response;
@track inputValue;
@wire(calloutForStack, { inputString: '$inputValue' })
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 + '...');
}
handleInputChange(event) { ;
}
handleButtonClick() {
console.log('apex/ApiFhirPost.handleButtonClick(1a.js)');
console.log('apex/ApiFhirPost.handleButtonClick(65)
Do calloutForStack()');
const inputEl = this.template.querySelector('lightning-input');
const inputValue1 = inputEl.value;
console.log("ApiFhirPost.handleButtonClick(77) input: " + inputValue1 + '...');
const myInputCl = this.template.querySelector('.clTextInput');
console.log("ApiFhirPost.handleButtonClick(78a) inputCl: " +
myInputCl.value);
var strOutputter = '';
console.log("ApiFhirPost.handleButtonClick(81) inputCl: " +
strOutputter);
var apiOutput = '';
calloutForStack({ inputString: this.inputValue })
.then(result => {
console.log("ApiFhirPost.handleButtonClick(86) input: " +
myInputCl.value + '...');
this.response = result;
console.log("ApiFhirPost.handleButtonClick(89) input: " +
result + '...');
console.log("ApiFhirPost.handleButtonClick(92) input: " +
JSON.stringify(result) + '...');
apiOutput = JSON.parse(result);
console.log("ApiFhirPost.handleButtonClick(94) data: " +
apiOutput.data + '...');
console.log("ApiFhirPost.handleButtonClick(97) Enough nonsense...");
})
.catch(error => {
console.error(error);
});
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.
.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.)
<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.
public class SendEmails{
public static void sendEmail(String sSendTo, String sSubject, String sBody)
{
System.debug('SendEmails.sendEmail().........................');
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[]{sSendTo};
String subject = sSubject;
email.setSubject(subject);
email.setToAddresses(toAddresses);
email.setPlainTextBody(sBody);
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.
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;}
public String strNameIdAddress()
{
String dname = '';
if(this.name.size() > 0)
{
dname = this.name[0].family + ', ';
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;
}
}
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;}
}
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;}
}
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_x {get;set;}
public String value {get;set;}
public String use {get;set;}
}
public class addressResource {
public addressResource()
{
line = new List<String>();
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;}
}
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 = '';
}
}
}
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.
public with sharing class ApiResponseClass1 {
@AuraEnabled public String sCompleteStatus{ get; set; }
@AuraEnabled public String sStatusCode{ get; set; }
@AuraEnabled public String sdata{ get; set; }
@AuraEnabled public String serror{ get; set; }
public ApiResponseClass1() { }
public ApiResponseClass1(String psStatusCode, String psdata, String pserror) {
this.sStatusCode = psStatusCode;
this.sdata = psdata;
this.serror = pserror;
}
public void setValues(String psStatusCode, String psdata, String pserror) {
this.sStatusCode = psStatusCode;
this.sdata = psdata;
this.serror = pserror;
}
public String getToJson(){
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:
httpResponse callout(String httpMethod, String endpoint, String body
It is generic to GET
, POST
, PUT
... This code could use touchups, but it works...
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) {
ApiResponseClass1 apiResponse = new ApiResponseClass1();
String outputString = 'FhirGet Processed string: ' + sInputString;
String strInput = sInputString;
String httpMethod = 'GET';
system.debug('Method ApiFhirGet.calloutForStack(1a) input:' + strInput + '...');
String endPoint = BASE_URL + 'Patient/8301013';
system.debug('Method ApiFhirGet.calloutForStack(1ep) endpoint: ' +
endPoint + '...');
String strReponseBody = '';
HttpResponse res = callout('GET', endPoint, '');
System.debug('calloutForStack(1ab)..');
Integer statusCode=res.getStatusCode();
System.debug('calloutForStack(1d) statusCode: ' + statusCode + '...');
Integer iStatus= res.getStatusCode();
String sStatus= iStatus.format();
apiResponse.sStatusCode = iStatus.format();
apiResponse.sCompleteStatus = res.toString();
String sResponse = '';
if(iStatus == 200){
System.debug('calloutForStack(1em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
strReponseBody = res.getBody();
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()) + '...');
for (String key : headerKeys)
{
String svalue = res.getHeader(key);
System.debug('calloutForStack(1fb) Header key=' +
key + '- value=' + svalue + '...');
}
Map<String, Object> result =
(Map<String, Object>)JSON.deserializeUntyped(strReponseBody);
Set <String> resSet = new Set<String>();
resSet = result.keySet();
List<String> lststr = new List<String>(resSet);
String sq = lststr[0];
System.debug('calloutForStack(1hp) Key: ' +
sq + '...');
sResponse = '{"Status":"' + sStatus + '"}';
System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');
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>');
apiResponse.sdata = RR.strNameIdAddress();
system.debug('calloutForStack(112): ' + apiResponse.sdata + '...');
}
else {
apiResponse.serror = '(2)Unknown Error GETing FHIR data';
}
sResponse = apiResponse.getToJson();
system.debug('Method ApiFhirGet.calloutForStack(9za) sResponse:' +
sResponse + '...');
return sResponse;
}
public static httpResponse callout
(String httpMethod, String endpoint, String body){
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()));
}
req.setHeader('Accept-Encoding','gzip, deflate');
req.setHeader('Content-Type','application/json');
req.setHeader('Accept','application/json');
req.setTimeout(120000);
req.setCompressed(true);
httpResponse res = new http().send(req);
system.debug(res.toString());
system.debug(res.getBody());
return res;
}
apiFhirPost.cls
Again, notice that the second method could be in its own class... probably... and used by all REST calls.
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;
}
@AuraEnabled(cacheable=true)
public static String calloutForStack(String sInputString) {
ApiResponseClass1 apiResponse = new ApiResponseClass1();
String outputString = 'FhirGet Processed string: ' + sInputString;
String strInput = sInputString;
String httpMethod = 'POST';
system.debug('Method ApiFhirPost.calloutForStack(1a) input:' +
strInput + '...');
String endPoint = BASE_URL + 'Patient';
system.debug('Method ApiFhirPost.calloutForStack(1ep) endpoint:' +
endPoint + '...');
String strRequestBody = '';
FHIRAPiClasses.patientResponseResource patient =
new FHIRAPiClasses.patientResponseResource();
System.debug('calloutForStack(48jab): ' + strRequestBody + '...');
patient.resourceType = 'Patient';
patient.id = '';
patient.name.Add(new FHIRAPiClasses.nameResource('official','Woo','David'));
patient.telecom.Add(new FHIRAPiClasses.telecomResource
('phone', '8012121756', 'mobile'));
patient.telecom.Add(new FHIRAPiClasses.telecomResource
('email', 'dwoo@lmicrosquish.com', ''));
patient.gender = 'male';
patient.birthdate = '2508-06-03';
patient.address.Add(new FHIRAPiClasses.addressResource
('200 CCatskinner Dr.', 'Slash City', 'TY', '54545'));
strRequestBody = JSON.serialize(patient);
System.debug('calloutForStack(1jab): ' + strRequestBody + '...');
System.debug('calloutForStack(1jac)..');
String strReponseBody = '';
String strData = '';
HttpResponse res = callout('POST', endPoint, strRequestBody);
String strRawReponse = res.getBody();
System.debug('calloutForStack(1ab)...' + strRawReponse);
Integer iStatus= res.getStatusCode();
apiResponse.sStatusCode = iStatus.format();
System.debug('calloutForStack(1d) statusCode: ' +
apiResponse.sStatusCode + '...');
apiResponse.sCompleteStatus = res.toString();
String sResponse = '';
if(iStatus == 201){
System.debug('calloutForStack(1em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
strReponseBody = res.getBody();
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()) + '...');
for (String key : headerKeys)
{
String svalue = res.getHeader(key);
System.debug('calloutForStack(1fb) Header key=' +
key + '- value=' + svalue + '...');
}
Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped
(strReponseBody);
sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
System.debug('calloutForStack(1epm) entireStatus: ' + sResponse + '...');
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>');
strData = RR.strNameIdAddress();
apiResponse.sdata = RR.strNameIdAddress();
system.debug('calloutForStack(112): ' + strData + '...');
}
else if (iStatus == 400) {
System.debug('calloutForStack(12em) entireStatus: ' +
apiResponse.sCompleteStatus + '...');
sResponse = '{"Status":"' + apiResponse.sStatusCode + '"}';
System.debug('calloutForStack(12epm) entireStatus: ' + sResponse + '...');
strReponseBody = res.getBody();
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 {
apiResponse.serror = '(2)Unknown Error POSTing FHIR data';
}
system.debug('calloutForStack(1p): Status=' + apiResponse.getToJson());
sResponse = apiResponse.getToJson();
system.debug('Method ApiFhirPost.calloutForStack(9za)
sResponse:' + sResponse + '...');
return sResponse;
}
public static httpResponse callout
(String httpMethod, String endpoint, String body){
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()));
}
req.setHeader('Accept-Encoding','gzip, deflate');
req.setHeader('Content-Type','application/json');
req.setHeader('Accept','application/json');
req.setTimeout(120000);
req.setCompressed(true);
httpResponse res = new http().send(req);
system.debug(res.toString());
system.debug(res.getBody());
return res;
}
}
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