Make your first Apex Rest Callout

Rafik Guennoun
Altius services
10 min readMar 18, 2024

--

As a Salesforce developer, you will inevitably need to manage data integration between Salesforce and other systems, whether it involves sending data from Salesforce or receiving data into Salesforce.

In this article, we’ll take a look into the fundamentals, focusing on a very important tool that every developer should have in their toolkit: Apex Callouts. We’ll explore how to write unit tests for them, as well as the best practices to follow throughout the integration process.

Apex Callouts

Apex callouts in Salesforce refer to the capability to initiate a call to an external web service or send an HTTP request from Apex code. This functionality enables seamless integration between Salesforce and external services and is commonly used for integrating Salesforce with other systems or retrieving data from external sources.

Salesforce’s proprietary programming language, Apex, smoothly integrates with web services using either SOAP and WSDL or HTTP services, commonly referred to as RESTful services.

For SOAP-based integration, Apex allows developers to generate classes from WSDL files, providing a structured approach to interact with web services following the Simple Object Access Protocol.

On the other hand, for HTTP services adhering to REST principles, developers can employ Apex to make HTTP requests, utilizing standard methods like GET, POST, PUT, and DELETE to communicate with web service endpoints.

  • GET : Used to retrieve resources from a server. It is a safe method as it does not change the state of the resource in any way
  • POST : Used to submit an entity to the specified resource, often causing a change in state or side effects on the server.
  • PUT : Used to update the existing resource on the server and it updates the full resource. If the resource does not exist, PUT may decide to create a new resource
  • DELETE : Used to delete the resources from a server. It deletes the resource identified by the Request-URI

A Request-URI (Uniform Resource Identifier) is a string of characters that identifies the resource upon which to apply an HTTP request.

Some advantages of REST services include their flexibility in handling data types because it’s Apex . You can extract all the data you need, including related objects. You can build your data structure and make the call to the endpoint.

However, on the flip side, there is no automatic forwarding, and messages that fail will disappear. Additionally, there are governor limits to consider.

In this article, our focus will be on REST services, reserving SOAP for future exploration.

The Log object

Before we try to do some apex calls, you have to meet your best friend in your integration journey “The Log object”. Salesforce does not provide a standard object to save and deal with errors, so we have to create a custom one to store our errors in order to know exactly what happens in our system in order to fix it later, either manually or by writing a batch that will relaunch the failed calls.

Here is a little description of the fields of the Log__c object :

Description of the Log__c object

For this object, we are going to create a helper class called LogUtils, for now we will write only the function that will help us to insert logs.

public class LogUtils {

public static createLog(Sobject obj, String source, String method, Integer statusCode, String body){

// obj : The specific record or data we aim to retrieve, create or remove
// source : The name of the external web service
// method : GET, POST, PUT, DELETE
// response : the HTTP Response

// If obj is null objName = Auth, else it will have the object name
String objName = obj.getSObjectType().getDescribe().getName();

Log__c log = new Log__c(
Key__c = obj.id,
Type__c = objName,
Source__c = source,
Value__c = statusCode,
Detail__c = objName + ' - Fail to ' + method,
Content__c = body
);

insert log;
}
}

Use Case

To be able to understand how we do apex calls, we have to put ourselves in a use case.

Let’s say that we are going to use the API called Cars Connect

Use case API informations

For this use case, we suppose that the car is created in Salesforce and only the License Number is filled, and then by using this License Number we will make a call to Cars Connect to get the rest of the information.

This schema explains how does the process works :

Integration process

This is the object where we will store the data :

Car custom object

Use case API Documentation

1. Authentication

Request

Endpoint : www.carsconnect.com/api/auth
Method : POST
// Header 
{
"Content-Type" : "application/json"
}
// Body 
{
"grant_type" : "client_credentials",
"client_id" : "client id here",
"client_secret" : "client secret here",
"scope" : "scope here"
}

Response

// Status Code : 200
// Body
{
"access_token": "String"
}

The access token will be used in the header of the requests

2. Get Car by License Number

Request

Endpoint : www.carsconnect.com/api/cars/licenseNumber
Method : GET
// Header 
{
"Authorization" : "Bearer accessToken",
"Content-Type" : "application/json"
}

Response

// Body
{
"licenseNumber": "String",
"brand": "String",
"name": "String",
"year": "Integer"
}

Exemple

// Result of the GET method for the car with the license number "123"
{
"licenseNumber": "123",
"brand": "Volkswagen",
"name": "Golf",
"year": "2023"
}

Save authentication information

Almost every web service needs authentication to be able to use it. There are a lot of authentication types, but for our use case as shown in the authentication json above, we used a type called OAuth 2.0 — Client Credentials Grant, one of the most used types and it’s used for machine-to-machine authentication.

  • grant_type: Specifies the type of permission you’re asking for. In this case, it’s saying, “I want to use client credentials to get access.”
  • client_id: A unique ID for your application, given by the service you’re connecting to. It’s like a username for your app.
  • client_secret: A secret password for your application, known only by your app and the service. It proves your app’s identity.
  • scope: Describes what your app wants to do. It specifies the kind of access it needs. For example, “read” or “write” access to certain data.

In order to authenticate to the Cars Connect or any web service, we have to store this information somewhere, never hardcode this kind of information.

For this, we have to create a Custom Settings called Cars Connector Credentials with the following fields, then click on Manage.

“Cars Connector Credentials” custom setting

We can store values the values in two manners, either :

  • Have a default value of the org and make it available for use by any profile or user
  • Store a different value for each profile or user

In our use case we will go for the first option, for this we will choose the New button on the top.

Fill with your api information and click on save, you will have something like this

Once our credentials are stored, we need to do one more thing to be able to use the web service before we start writing code

Go to Remote Site Settings and create a new one called CarsConnect that contains the base URL of our API (the initial part of the API URL which is www.carsconnect.com), this will be used to authorize and establish connections between Salesforce and the external server

Apex code

One of the best practices of coding is avoiding writing the same code multiple times. It is better to write it only once because this will make it easier to maintain.

For this we will create an apex class called CarsConnectConnector that will contain all our code

public class CarsConnectConnector {

// Instance of the CarsConnectCrendential custom setting
final static CarsConnectCredentials__c credentials = CarsConnectCredentials__c.getInstance();

}

One of the codes that we will use a lot is the code to prepare a GET or a POST request, we will add this two functions to the CarsConnectConnector class

private static HttpRequest preparePostRequest(String endpoint, String body, String accessToken) {

HttpRequest httpRequest = new HttpRequest();

httpRequest.setEndpoint(endpoint);
httpRequest.setMethod('POST');
httpRequest.setTimeout(120000);

httpRequest.setHeader('Content-Type','application/json');

if(accessToken != null){
httpRequest.setHeader('Authorization','Bearer ' + accessToken);
}

httpRequest.setBody(body);

return httpRequest;
}

private static HttpRequest prepareGetRequest(String endpoint, String accessToken) {

HttpRequest httpRequest = new HttpRequest();

httpRequest.setEndpoint(endpoint);
httpRequest.setMethod('GET');
httpRequest.setTimeout(120000);

httpRequest.setHeader('Authorization','Bearer ' + accessToken);
httpRequest.setHeader('Content-Type','application/json');

return httpRequest;
}

Once the requests are ready, we will prepare the functions that send these requests to the CarsConnect web service :

  • If we got a response we will return it.
  • If it fails to get a response we will store that error in a Log record.
private static String sendRequest(HttpRequest request, Sobject obj) {

String responseBody = '';
Integer statusCode = 0;

try {

Http http = new Http();
HttpResponse response = http.send(request);

responseBody = response.getBody();
System.debug('body = ' + responseBody);

statusCode = response.getStatusCode();
System.debug('statusCode = ' + statusCode);

return responseBody;

} catch(Exception e) {

LogUtils.createLog(
obj,
'Cars Connect',
request.getMethod(),
statusCode,
responseBody
);

insert log;
return null;
}
}

Then we will create the method that returns to us the authnetication access token

private static String auth(){

String authEndpoint = credentials.Endpoint__c + '/auth';

// prepare authetication data
Map<String, String> authData = new Map<String, String>{
'grant_type' => credentials.GrantType__c,
'client_id' => credentials.ClientID__c,
'client_secret' => credentials.ClientSecret__c,
'scope' => credentials.Scope__c
};

// Convert it from Map to Json
String body = JSON.serialize(authData);

HttpRequest request = preparePostRequest( authEndpoint, body,null);

String responseBody = '';
Integer statusCode = 0;

try {

Http http = new Http();

HttpResponse response = http.send(request);

responseBody = response.getBody();
System.debug('body = ' + responseBody);

statusCode = response.getStatusCode();
System.debug('statusCode = ' + statusCode);

Map<String, Object> resBody = (Map<String, Object>) JSON.deserializeUntyped(res);

if (!resBody.containsKey('access_token')) {
return null;
}

String accessToken = String.valueOf(resBody.get('access_token'));
return accessToken;

} catch(Exception e) {

LogUtils.createLog(
'Auth',
'Cars Connect',
'POST',
statusCode,
responseBody
);

insert log;
return null;
}
}

Now that our helper functions are ready let’s write the code that gets us car data using the license number

  • Get car data from Cars Connect :
private static String getCarData(Car__c car, String accessToken){

String endpoint = credentials.Endpoint__c + '/cars/' + car.LicenseNumber__c;

HttpRequest request = prepareGetRequest(endpoint, accessToken);

String response = sendRequest(request, car);

return response;
}
  • Prepare the car record that we will update
private static Car__c prepareCarRecord(Car__c car, String response){

Car__c carToUpdate = [select id, licenseNumber__c, Brand__c, Name__c, Year__c
from Car__c where licenseNumber__c = :car.licenseNumber__c
limit 1];

Map<String, Object> carData = (Map<String, Object>) JSON.deserializeUntyped(response);

carToUpdate.Brand__c = String.valueOf(carData.get('brand'));
carToUpdate.Name__c = String.valueOf(carData.get('name'));
carToUpdate.Year__c = Integer.valueOf(carData.get('year'));

return carToUpdate;
}
  • Update the car record
public static void updateCar(Car__c car, String accessToken){

String carData = getCarData(car, accessToken);

Car__c carToUpdate = prepareCarRecord(car,carData);

update carToUpdate;
}

Unit Tests

Unit tests are automated tests that check if individual pieces of your code, like classes or methods, work as intended. They’re essential because they catch errors early, ensuring your code is reliable and meets requirements.

Salesforce mandates a minimum test coverage for deploying code, making unit tests a necessary part of maintaining a stable and high-quality application.

For this we will create a class called CarsConnectConnectorTest with the annotation @isTest

@isTest
public class CarsConnectConnectorTest {}

Next, we need to prepare our test data in a setup function with the annotation @testSetup

@testSetup 
static void setup() {

Car__c car = new Car__c(
licenseNumber__c = '123'
);

insert car;
}

Then we need to prepare our HttpCalloutMock.

In Salesforce, HttpCalloutMock is a class used in Apex testing to simulate HTTP callouts in a controlled and predictable manner.

When testing code that makes HTTP requests, instead of making actual external calls, we use an HttpCalloutMock class to define the expected response that our code would receive from the external service.

This allows us to create unit tests for scenarios involving web service calls without relying on the actual external service. By implementing the HttpCalloutMock interface, we provide a mock response for the HTTP request made by our code during testing. This ensures that our tests are not dependent on the external service’s availability, response variations, or potential rate limits.

It also allows us to test different scenarios, such as success and error responses, to ensure that our code can handle various situations when interacting with external APIs or web services.

For this we will add an inner class called CarDataMock inside the CarsConnectConnectorTest class

private class CarDataMock implements HttpCalloutMock{

public HTTPResponse respond(HTTPRequest req) {

HttpResponse res = new HttpResponse();

res.setHeader('Content-Type', 'application/json');

Map<String, Object> carData = new Map<String, Object>{
'licenseNumber' => '123',
'brand' => 'Volkswagen',
'name' => 'Golf',
'yeaer' => 2023
};

string bodyData = JSON.serialize(carData);

res.setBody(bodyData);
res.setStatusCode(200);
return res;
}
}

Finally, it's time to write our test function testGetCarData with the annotation @isTest

@isTest
static void testUpdateCar(){

Car__c car = [SELECT id, licenseNumber__c, Brand__c, Name__c, Year__c
FROM Car__c
WHERE licenseNumber__c = '123'];

// Check that the car doesn't have a name yat
system.assertEquals(car.Name__c, Null, 'Car already have a name');

CarDataMock successMock = new CarDataMock();
Test.setMock(HttpCalloutMock.class, successMock);

Test.startTest();
CarsConnectConnector.updateCar(car, 'fakeAccessToken');
Test.stopTest();

Car__c updatedCar = [SELECT id, licenseNumber__c, Brand__c, Name__c, Year__c
FROM Car__c
WHERE licenseNumber__c = '123'];

// The fields are updated correctly
system.assertEquals(car.Name__c, 'Golf', 'Name didnt change');
system.assertEquals(car.Brand__c, 'Volkswagen', 'Brand didnt change');
system.assertEquals(car.Year__c, 2023, 'Year didnt change');
}

Conclusion

In this article, we’ve explored the various aspects of Apex REST callouts, highlighting essential implementation details, best practices and the critical role of unit testing.

Hopefully this tutorial was helpful, let me know if you have any question in the comments.

--

--