Apex REST Callouts Architecture Concept
Welcome back to our blog! Today I wanna give you an interesting concept of Apex REST callouts architecture. Ready? So fasten your seat belts and enjoy!
Introduction
What is an Apex Callout? According to Salesforce documentation:
An Apex callout enables you to tightly integrate your Apex with an external service by making a call to an external Web service or sending a HTTP request from Apex code and then receiving the response. Apex provides integration with Web services that utilize SOAP and WSDL, or HTTP services (RESTful services).
Salesforce
Apex provides mainly three built-in classes to work with HTTP services and create HTTP requests:
- Http Class – used to initiate an HTTP request and response.
- HttpRequest Class – used to programmatically create HTTP requests like GET, POST, PATCH, PUT, and DELETE.
- HttpResponse Class – used to handle the HTTP response returned by HTTP
Apex REST Callouts Architecture Concept
All 3 classes above are necessary to make HTTP callouts. If you have a larger project with a separate class for each integration, you should notice that the code is duplicated in some places. Any ideas on how to improve that?
Let’s create an abstract class for common HTTP methods:
public abstract with sharing class CalloutClient {
protected final String HEADER_CONTENT_TYPE = 'Content-Type';
protected final String HEADER_CONTENT_TYPE_APPLICATION_JSON = 'application/json';
protected final String HEADER_AUTHORIZATION = 'Authorization';
protected final String HEADER_BEARER_AUTHORIZATION = 'Bearer';
protected final String HTTP_METHOD_GET = 'GET';
protected final String HTTP_METHOD_POST = 'POST';
protected final Integer DEFAULT_TIMEOUT = 120000;
protected HttpRequest request;
protected HttpResponse response;
protected void doCallout() {
this.response = new Http().send(request);
}
protected Boolean isStatusCodeOk() {
return response.getStatusCode() == 200;
}
protected Integer getResponseStatusCode() {
return response.getStatusCode();
}
protected String getResponseBody() {
return response.getBody();
}
protected virtual HttpRequest createRequest(String method) {
request = new HttpRequest();
request.setMethod(method);
return request;
}
protected abstract Object getResponseObject();
public class CalloutClientException extends Exception {}
}
Now we want to create a class for the integration, with some custom logic, that handles the HTTP response in a proper way. Let’s call it the Client. That class extends our CalloutClient class.
public with sharing class StripeChargeCardCalloutClient extends CalloutClient {
private static final String BASE_ENDPOINT = 'callout:StripeBaseEndpoint';
private static final String TOKEN = StripeConfig__mdt.getInstance('Token').Value__c;
private static final String SUCCESS_CHARGE_STATUS = 'succeeded';
private static String ENDPOINT = '/charges';
private String chargeCardRequest;
public StripeChargeCardCalloutClient() {}
public CardResponse chargeCard(Integer chargeAmount, String chargeCurrency, String chargeSource) {
generateRequest(chargeAmount, chargeCurrency, chargeSource);
createRequest();
doCallout();
return handleResponse();
}
public override Object getResponseObject() {
try {
return JSON.deserialize(getResponseBody(), CardResponse.class);
} catch (JSONException ex) {
throw new JSONException('Response deserialization has failed.');
}
}
private void generateRequest(Integer chargeAmount, String chargeCurrency, String chargeSource) {
chargeCardRequest = 'amount=' + chargeAmount +
'¤cy=' + EncodingUtil.urlEncode(chargeCurrency, 'UTF-8') +
'&source=' + EncodingUtil.urlEncode(chargeSource, 'UTF-8');
}
private void createRequest() {
request = super.createRequest(HTTP_METHOD_POST);
request.setEndpoint(BASE_ENDPOINT + ENDPOINT);
request.setHeader(HEADER_AUTHORIZATION, HEADER_BEARER_AUTHORIZATION + ' ' + TOKEN);
request.setBody(chargeCardRequest);
}
private CardResponse handleResponse() {
if (isStatusCodeOk()) {
return (CardResponse) getResponseObject();
} else {
throw new CalloutClientException('Invalid status code: ' + getResponseStatusCode());
}
}
public class CardResponse {
public String id;
public String status;
}
}
You can easily call the Client class from anywhere. But you can also group your Client classes (based on similar functions, external systems, etc.) into one Service class so you get one extra level of abstraction to help you organize your code better.
public with sharing class StripeCalloutService {
public StripeCalloutService() {}
public StripeChargeCardCalloutClient.CardResponse chargeCard(Integer chargeAmount, String chargeCurrency, String chargeSource) {
return new StripeChargeCardCalloutClient().chargeCard(chargeAmount, chargeCurrency, chargeSource);
}
public StripeGetPriceCalloutClient.PriceResponse getPrice(String priceId) {
return new StripeGetPriceCalloutClient().getPrice(priceId);
}
}
All the code is here – Callout Framework. You can easily clone it and test it in your environment. Here are examples of running the integration using the Service class:
- new StripeCalloutService().chargeCard(100, ‘USD’, ‘tok_mastercard’);
- new StripeCalloutService().getPrice(‘price_1Ign8P2eZvKYlo2ClEXjlnne’);
Callout Notes, Limits and Limitations
- Before any Apex callout can call an external site, that site must be registered in the Remote Site Settings page, or the callout fails. Salesforce prevents calls to unauthorized network addresses.
- If the callout specifies a named credential as the endpoint, you don’t need to configure remote site settings. A named credential specifies the URL of a callout endpoint and its required authentication parameters in one definition.
- A single Apex transaction can make a maximum of 100 callouts to an HTTP request or an API call.
- The default timeout is 10 seconds. A custom timeout can be defined for each callout. The minimum is 1 millisecond and the maximum is 120,000 milliseconds. See the examples in the next section for how to set custom timeouts for Web services or HTTP callouts.
- The maximum cumulative timeout for callouts by a single Apex transaction is 120 seconds. This time is additive across all callouts invoked by the Apex transaction.
- Every org has a limit on long-running requests that run for more than 5 seconds (total execution time). HTTP callout processing time is not included when calculating this limit. We pause the timer for the callout and resume it when the callout completes. See Execution Governors and Limits for Lightning Platform Apex limits.
- You can’t make a callout when there are pending operations in the same transaction. Things that result in pending operations are DML statements, asynchronous Apex (such as future methods and batch Apex jobs), scheduled Apex, or sending email. You can make callouts before performing these types of operations.
- Pending operations can occur before mock callouts in the same transaction. See Performing DML Operations and Mock Callouts for WSDL-based callouts or Performing DML Operations and Mock Callouts for HTTP callouts.
- When the header Expect: 100-Continue is added to a callout request and a HTTP/1.1 100 Continue response isn’t returned by the external server, a timeout occurs.
Was it helpful? Check out our other great posts here.
Resources
- https://bitbucket.org/salesforceprofs/callout-framework/src/master/
- https://stripe.com/docs/api
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts.htm
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http.htm
- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_timeouts.htm
It is really very good article. Thank you so much to the team.