Apex Integration Services
These are technical notes I compiled while studying using Trailhead, Salesforce's free self-learning portal.
Apex Integration Overview
Describe the differences between web service and HTTP callouts. Authorize an external site with remote site settings.- Make Callouts to External Services from Apex
- Apex Callouts enable you to tightly integrate Apex code with an external service. Two basic types:
- SOAP: Web service callouts to SOAP use XML, and usually require a WSDL (“wiz-dull”) document for code generation
- REST: HTTP callouts typically use REST with JSON
- These types are similar in terms of sending a request and receiving a response, but:
- WSDL-based callouts apply to SOAP
- HTTP callouts work with any HTTP service (SOAP or REST)
- Whenever possible, use an HTTP service
- Easier to interact with, require less code, and utilize easily readable JSON
- REST is newer and more commonly used
- SOAP is mostly used when integrating with legacy applications or transactions that require a formal exchange format or stateful interactions
- Apex Callouts enable you to tightly integrate Apex code with an external service. Two basic types:
- Authorize Endpoint Addresses
- Any time you make a callout to an external site Salesforce makes sure it is authorized
- So, before you start working with callouts, update the list of approved sites for your org on the Remote Site Settings page
- Setup > Quick Find > “Remote Site” > Remote Site Settings
- Adding a remote site URL such as
https://th-apex-http-callout.herokuapp.com
authorizes all subfolders for the endpoint, likehttps://th-apex-http-callout.herokuapp.com/path1
- Adding a remote site URL such as
- Any time you make a callout to an external site Salesforce makes sure it is authorized
Apex REST Callouts
Perform a callout to receive data from an external service. Perform a callout to send data to an external service. Test callouts by using mock callouts.- HTTP And Callout Basics
- REST callouts are based on HTTP
- Each callout request is associated with an HTTP method and an endpoint
- Simplest request is a
GET
request (GET
is an HTTP method). Other methods follow:GET
: Retrieve data identified by a URLPOST
: Create a resource or post data to the serverDELETE
: Delete a resource identified by a URLPUT
: Create or replace the resource sent in the request body
- Each request also sets a URI, which is the endpoint address where the service is located
- Example from Trailhead: https://th-apex-http-callout.herokuapp.com/animals
- When the server processes the request, it sends a status code in the response. Example codes:
200
: request successful404
: file not found500
: internal service error
- Get Data from a Service
- Note that accessing the URL above returns JSON formatted like the following
{"animals":["majestic badger","fluffy bunny","scary bear","chicken"]}
- Service sends the response in JSON format, which is essentially a string. The following code uses the built-in
JSONParser
class to convert it to an object.- Run it using an Execute Anonymous window to see the result below
- JSON for this example is simple and pretty easy to parse
- For more complex JSON structures, you can use JSON2Apex
- This tool generates strongly typed Apex code for parsing JSON structures - just paste in the JSON and it generates the Apex for parsing it
- Note that accessing the URL above returns JSON formatted like the following
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
request.setMethod('GET');
HttpResponse response = http.send(request);
// If the request is successful, parse the JSON response.
if(response.getStatusCode() == 200) {
// Deserialize the JSON string into collections of primitive data types.
Map<String, Object> results=(Map<String, Object>)JSON.deserializeUntyped(response.getBody());
// Cast the values in the 'animals' key as a list
List<Object> animals = (List<Object>) results.get('animals');
System.debug('Received the following animals:');
for(Object animal: animals) {
System.debug(animal);
}
}
- Send Data to a Service
POST
ing data to a service is sending data to the service- Example below sends a
POST
request to the web service to add an animal name- New name is sent as a JSON string
Content-Type
header is set to let the service know the sent data is in JSON format so it can process the data appropriately
- If request was processed successfully, returns status code
201
, indicating a resource has been created
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
// Set the body as a JSON object
request.setBody('{"name":"mighty moose"}');
HttpResponse response = http.send(request);
// Parse the JSON response
if(response.getStatusCode() != 201) {
System.debug('The status code returned was not expected: '
+ response.getStatusCode()
+ ' '
+ response.getStatus());
} else {
System.debug(response.getBody());
}
- Test Callouts
- Apex test methods do not support callouts - tests that perform callouts fail
- But, the testing runtime allows you to “mock” the callout, which allows you to specify the response to return in the test instead of actually calling the web service
- Essentially, you are telling the runtime “The web service will return X. Instead of calling the service, just return X.”
- Using mock callouts helps ensure you attain adequate code coverage and that no lines of code are skipped due to callouts
- Prerequisites
- Following class contains modified versions of the
GET
andPOST
request examples executed anonymously in the previous section - Dev Console > File > New > Apex Class, then use class name: “AnimalsCallout”
- Following class contains modified versions of the
AnimalsCallouts.apxc
public class AnimalsCallouts {
public static HttpResponse makeGetCallout() {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
request.setMethod('GET');
HttpResponse response = http.send(request);
// If the request is successful, parse the JSON response.
if(response.getStatusCode() == 200) {
// Deserializes the JSON string into collections of primitive data types.
Map<String, Object> results = (
(Map<String, Object>) JSON.deserializeUntyped(response.getBody()));
// Cast the values in the 'animals' key as a list
List<Object> animals = (List<Object>) results.get('animals');
System.debug('Received the following animals:');
for(Object animal: animals) {
System.debug(animal);
}
}
return response;
}
public static HttpResponse makePostCallout() {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setBody('{"name":"mighty moose"}');
HttpResponse response = http.send(request);
// Parse the JSON response
if(response.getStatusCode() != 201) {
System.debug('The status code returned was not expected: ' +
response.getStatusCode() + ' ' + response.getStatus());
} else {
System.debug(response.getBody());
}
return response;
}
}
- Test a Callout with StaticResourceCalloutMock
- To test callouts, use mock callouts by either implementing an interface or using static resources. This example uses static resources and a mock interface later on.
- Apex runtime knows to look up the response specified in the static resource and returns it
Test.setMock
method informs the runtime that mock callouts are used in the test method- Set up via: Dev Console > File > New > Static Resource
- Call it
GetAnimalResource
- Select MIME type
text/plain
for the JSON - Add the following content to the text file:
{"animals": ["pesky porcupine", "hungry hippo", "squeaky squirrel"]}
- Call it
- Now, create a test for callout that uses that resource
- Set up via: Dev Console > File > New > Apex Class
- Check the checkbox “Always Run Asynchronously” under the Test menu
- Run the test via: Test > New Run
- To test callouts, use mock callouts by either implementing an interface or using static resources. This example uses static resources and a mock interface later on.
@isTest
private class AnimalsCalloutsTest {
@isTest static void testGetCallout() {
// Create the mock response based on a static resource
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('GetAnimalResource');
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json;charset=UTF-8');
// Associate the callout with a mock response
Test.setMock(HttpCalloutMock.class, mock);
// Call method to test
HttpResponse result = AnimalsCallouts.makeGetCallout();
// Verify mock response is not null
System.assertNotEquals(null,result,
'The callout returned a null response.');
// Verify status code
System.assertEquals(200,result.getStatusCode(), 'The status code is not 200.');
// Verify content type
System.assertEquals('application/json;charset=UTF-8',
result.getHeader('Content-Type'),
'The content type value is not expected.');
// Verify the array contains 3 items
Map<String, Object> results = (Map<String, Object>)
JSON.deserializeUntyped(result.getBody());
List<Object> animals = (List<Object>) results.get('animals');
System.assertEquals(3, animals.size(),
'The array should only contain 3 items.');
}
}
- Test a Callout with HttpCalloutMock
- To test your
POST
callout, this section provides an implementation of theHttpCalloutMock
interface- Interface enables you to specify the response that’s sent in the
respond
method - Test class instructs Apex runtime to send this fake response by calling
Test.setMock
again - First argument, pass
HttpCalloutMock.class
- Second argument, pass a new instance of
AnimalsHttpCalloutMock
, an implementation ofHttpCalloutMock
- Interface enables you to specify the response that’s sent in the
- First, create new Apex class
AnimalsHttpCalloutMock
, as shown below. - Then modify the test class
AnimalsCalloutsTest
to add the second test method,testPostCallout
, shown below. - Run the tests
- To test your
AnimalsHttpCalloutMock.apxc
@isTest
global class AnimalsHttpCalloutMock implements HttpCalloutMock {
// Implement this interface method
global HTTPResponse respond(HTTPRequest request) {
// Create a fake response
HttpResponse response = new HttpResponse();
response.setHeader('Content-Type', 'application/json');
response.setBody('{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}');
response.setStatusCode(200);
return response;
}
}
Updated AnimalsCalloutsTest.apxc
@isTest
private class AnimalsCalloutsTest {
@isTest static void testGetCallout() {
// Create the mock response based on a static resource
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('GetAnimalResource');
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json;charset=UTF-8');
// Associate the callout with a mock response
Test.setMock(HttpCalloutMock.class, mock);
// Call method to test
HttpResponse result = AnimalsCallouts.makeGetCallout();
// Verify mock response is not null
System.assertNotEquals(null,result,
'The callout returned a null response.');
// Verify status code
System.assertEquals(200,result.getStatusCode(), 'The status code is not 200.');
// Verify content type
System.assertEquals('application/json;charset=UTF-8',
result.getHeader('Content-Type'),
'The content type value is not expected.');
// Verify the array contains 3 items
Map<String, Object> results = (Map<String, Object>)
JSON.deserializeUntyped(result.getBody());
List<Object> animals = (List<Object>) results.get('animals');
System.assertEquals(3, animals.size(),
'The array should only contain 3 items.');
}
@isTest static void testPostCallout() {
// Set mock callout class
Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());
// This causes a fake response to be sent
// from the class that implements HttpCalloutMock.
HttpResponse response = AnimalsCallouts.makePostCallout();
// Verify that the response received contains fake values
String contentType = response.getHeader('Content-Type');
System.assert(contentType == 'application/json');
String actualValue = response.getBody();
System.debug(response.getBody());
String expectedValue = '{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}';
System.assertEquals(expectedValue, actualValue);
System.assertEquals(200, response.getStatusCode());
}
}
- The class below calls the URL https://th-apex-http-callout.herokuapp.com/animals/1, where
1
is an ID parameter that can be changed - Then, it parses the resulting JSON response, for example:
{"animal":{"id":1,"name":"chicken","eats":"chicken food","says":"cluck cluck"}}
AnimalLocator.apxc
public class AnimalLocator {
public static String getAnimalNameById(Integer intId) {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals/' + intId);
request.setMethod('GET');
HttpResponse response = http.send(request);
if(response.getStatusCode() != 200) {
System.debug('The status code returned was not expected: ' +
response.getStatusCode() + ' ' + response.getStatus());
} else {
Map<String, Object> results = ((Map<String, Object>)
JSON.deserializeUntyped(response.getBody()));
Map<String, Object> animal = (Map<String, Object>) results.get('animal');
String name = (String) animal.get('name');
return(name);
}
return('error');
}
}
The following code can be used to call AnimalLocator
in an Execute Anonymous window
AnimalLocator al = new AnimalLocator();
System.debug(al.getAnimalNameById(1));
It returns the following:
The following test code results in 100% test coverage of the AnimalLocator
class
AnimalLocatorMock.apxc
@isTest
global class AnimalLocatorMock implements HttpCalloutMock {
// Implement this interface method
global HTTPResponse respond(HTTPRequest request) {
// Create a fake response
HttpResponse response = new HttpResponse();
response.setHeader('Content-Type', 'application/json');
response.setBody('{"animal":{"id":1,"name":"chicken","eats":"chicken food","says":"cluck cluck"}}');
response.setStatusCode(200);
return response;
}
}
AnimalLocatorTest.apxc
@isTest
private class AnimalLocatorTest {
@isTest static void testGetCallout() {
// Set mock callout class
Test.setMock(HttpCalloutMock.class, new AnimalLocatorMock());
// This causes a fake response to be sent
// from the class that implements HttpCalloutMock
String returnValue = AnimalLocator.getAnimalNameById(1);
System.assert(returnValue == 'Chicken');
}
}
Apex SOAP Callouts
Generate Apex classes using WSDL2Apex. Perform a callout to send data to an external service using SOAP. Test callouts by using mock callouts.- Use WSDL2Apex to Generate Apex Code
- Apex can also make callouts to SOAP web services using XML, but working with SOAP can be painful
- Download the web service’s WSDL file, then upload the WSDL, and WSDL2Apex generates the Apex classes for you
- Apex classes construct the SOAP XML, transmit the data, and parse the response XML into Apex objects
- Works similar to WSDL2Java or importing a WSDL as a Web Reference in .NET
- Note: use outbound messaging to handle integration solutions when possible - use third-party web services only when necessary
- The Apex classes generated by WSDL2Apex internally handle the logic to construct and parse the XML of the web service messages
- Apex classes construct the SOAP XML, transmit the data, and parse the response XML into Apex objects
- This example uses a calculator.xml WSDL file
- Generate an Apex Class from the WSDL
- To use the WSDL2Apex tool, go to Setup > Quick Find > “Apex Classes” > Apex Classes, then click the “Generate from WSDL” button, upload the XML, then click “Generate Apex”
- The final page of the wizard shows the generated classes
- The calculator.xml file generated two Apex classes:
AsyncCalculatorServices
andcalculatorServices
- Execute the Callout
- Now, execute the callout and see if it correctly adds two numbers using the following code in Execute Anonymous
calculatorServices.CalculatorImplPort calculator = new calculatorServices.CalculatorImplPort();
Double x = 1.0;
Double y = 2.0;
Double result = calculator.doAdd(x,y);
System.debug(result);
- Test Web Service Callouts
- The 75% test code coverage requirement includes classes generated by WSDL2Apex, but test methods don’t support web service callouts
- To prevent tests from cailing and to increase code coverage, Apex provides a built-in
WebServiceMock
interface and theTest.setMock
method - Use this interface to receive fake responses in a test method, thereby providing the necessary test coverage
- Specify a Mock Response for Callouts
- When you create an Apex class from a WSDL, the methods in the class call
WebServiceCallout.invoke
, which performs the callout to the services - When testing these methods, you can instruct the Apex runtime to generate a fake response whenever
WebServiceCallout.invoke
is called- To do so, implement the
WebServiceMock
interface and specify a fake response for the testing runtime to send
- To do so, implement the
- Instruct the Apex runtime to send this fake response by calling
Test.setMock
in your test method- First argument:
WebServiceMock.class
- Second argument: new instance of your
WebServiceMock
interface implementation
- First argument:
- When you create an Apex class from a WSDL, the methods in the class call
Test.setMock(WebServiceMock.class, new MyWebServiceMockImpl());
- Full worked example:
- Dev Console > File > New > Apex Class >
AwesomeCalculator
AwesomeCalculator.apxc
public class AwesomeCalculator {
public static Double add(Double x, Double y) {
calculatorServices.CalculatorImplPort calculator =
new calculatorServices.CalculatorImplPort();
return calculator.doAdd(x,y);
}
}
- Dev Console > File > New > Apex Class >
CalculatorCalloutMock
CalculatorCalloutMock.apxc
@isTest
global class CalculatorCalloutMock implements WebServiceMock {
global void doInvoke(
Object stub,
Object request,
Map<String, Object> response,
String endpoint,
String soapAction,
String requestName,
String responseNS,
String responseName,
String responseType) {
// start - specify the response you want to send
calculatorServices.doAddResponse response_x =
new calculatorServices.doAddResponse();
response_x.return_x = 3.0;
// end
response.put('response_x', response_x);
}
}
- Dev Console > File > New > Apex Class >
AwesomeCalculatorTest
AwesomeCalculatorTest.apxc
@isTest
private class AwesomeCalculatorTest {
@isTest static void testCallout() {
// This causes a fake response to be generated
Test.setMock(WebServiceMock.class, new CalculatorCalloutMock());
// Call the method that invokes a callout
Double x = 1.0;
Double y = 2.0;
Double result = AwesomeCalculator.add(x, y);
// Verify that a fake result is returned
System.assertEquals(3.0, result);
}
}
- Run all tests with Dev Console > Test > Run All
- Result is 100% code coverage on the
AwesomeCalculator
class
- Result is 100% code coverage on the
Worked example:
- Generate an Apex class using this WSDL file
ParkService.apxc
//Generated by wsdl2apex
public class ParkService {
public class byCountryResponse {
public String[] return_x;
private String[] return_x_type_info = new String[]{'return','http://parks.services/',null,'0','-1','false'};
private String[] apex_schema_type_info = new String[]{'http://parks.services/','false','false'};
private String[] field_order_type_info = new String[]{'return_x'};
}
public class byCountry {
public String arg0;
private String[] arg0_type_info = new String[]{'arg0','http://parks.services/',null,'0','1','false'};
private String[] apex_schema_type_info = new String[]{'http://parks.services/','false','false'};
private String[] field_order_type_info = new String[]{'arg0'};
}
public class ParksImplPort {
public String endpoint_x = 'https://th-apex-soap-service.herokuapp.com/service/parks';
public Map<String,String> inputHttpHeaders_x;
public Map<String,String> outputHttpHeaders_x;
public String clientCertName_x;
public String clientCert_x;
public String clientCertPasswd_x;
public Integer timeout_x;
private String[] ns_map_type_info = new String[]{'http://parks.services/', 'ParkService'};
public String[] byCountry(String arg0) {
ParkService.byCountry request_x = new ParkService.byCountry();
request_x.arg0 = arg0;
ParkService.byCountryResponse response_x;
Map<String, ParkService.byCountryResponse> response_map_x = new Map<String, ParkService.byCountryResponse>();
response_map_x.put('response_x', response_x);
WebServiceCallout.invoke(
this,
request_x,
response_map_x,
new String[]{endpoint_x,
'',
'http://parks.services/',
'byCountry',
'http://parks.services/',
'byCountryResponse',
'ParkService.byCountryResponse'}
);
response_x = response_map_x.get('response_x');
return response_x.return_x;
}
}
}
- The following
ParkLocator
class contains a method calledcountry
that uses theParkService
class.
public class ParkLocator {
public static String[] country(String country){
ParkService.ParksImplPort parks = new ParkService.ParksImplPort();
String[] parksname = parks.byCountry(country);
return parksname;
}
}
- The following mock class
ParkServiceMock
mocks the callout response:
@isTest
global class ParkServiceMock implements WebServiceMock {
global void doInvoke(
Object stub,
Object request,
Map<String, Object> response,
String endpoint,
String soapAction,
String requestName,
String responseNS,
String responseName,
String responseType) {
// start - specify the response you want to send
ParkService.byCountryResponse response_x =
new ParkService.byCountryResponse();
List<String> myStrings = new List<String> {'Park1','Park2','Park3'};
response_x.return_x = myStrings;
// end
response.put('response_x', response_x);
}
}
- The following test class
ParkLocatorTest
contains unit tests for theParkService
class
@isTest
private class ParkLocatorTest {
@isTest static void testCallout() {
// This causes a fake response to be generated
Test.setMock(WebServiceMock.class, new ParkServiceMock());
// Call the method that invokes a callout
List<String> result = new List<String>();
List<String> expectedvalue = new List<String>{'Park1','Park2','Park3'};
result = ParkLocator.country('India');
// Verify that a fake result is returned
System.assertEquals(expectedvalue, result);
}
}
Apex Web Services
Describe the two types of Apex web services and provide a high-level overview of these services. Create an Apex REST class that contains methods for each HTTP method. Invoke a custom Apex REST method with an endpoint. Pass data to a custom Apex REST method by sending a request body in JSON format. Write a test method for an Apex REST method and set properties in a test REST request. Write a test method for an Apex REST method by calling the method with parameter values.- Expose Your Apex Class as a Web Service
- You can expose your Apex class methods as a REST or SOAP web service operation - by making methods callable through the web, your external applications can integrate with Salesforce
- For example, imagine your company’s call center uses an internal application to manage resources
- Custom support representatives are expected to use the same application to perform their daily work, including managing case records in Salesforce
- By using one interface, reps can view and update case records and access internal resources
- Expose a Class as a REST Service
- Making Apex classes available as a REST web service by:
- Defining your class as global
- Defining your methods as global static
- Add the following annotations to the class and methods. Ex: sample Apex REST class below uses one method
getRecord
method is a custom REST API call- Annotated with
@HttpGet
and is invoked for a GET request - Class is annotated with
@RestResource(urlMapping='/Account/*')
- Base endpoint for Apex REST is
https://yourInstance.my.salesforce.com/services/apexrest
For this org, it could look likehttps://yourInstance.my.salesforce.com/services/apexrest/Account/*
- Base endpoint for Apex REST is
- URL mapping is case-sensitive and can contain a wildcard character (
*
)
- Making Apex classes available as a REST web service by:
Annotation | Action | Details |
---|---|---|
@HttpGet |
Read | Reads or retrieves records |
@HttpPost |
Create | Creates records |
@HttpDelete |
Delete | Deletes records |
@HttpPut |
Upsert | Typically used to update existing records or create records |
@HttpPatch |
Update | Typically used to update fields in existing records |
@RestResource(urlMapping='/Account/*')
global with sharing class MyRestResource {
@HttpGet
global static Account getRecord() {
// Add your code
}
}
- Expose a Class as a SOAP Service
- Make your Apex class available as a SOAP web service by:
- Defining your class as gobal
- Add keyword
webservice
and thestatic
definition modifier to each methodwebservice
keyword provides global access to the method it is related to
- Example below is a sample class with one method
- External application can call your custom Apex methods as web service operations by consuming the class WSDL file
- Generate this WSDL file for you class from the class detail page, access from the Apex Classes page in Setup. Typically, send the WSDL file to third-party developers to write integrations for your web service
- The web service will require authentication, using either the Enterprise WSDL or Partner WSDL for login functionality
- Make your Apex class available as a SOAP web service by:
global with sharing class MySOAPWebService {
webservice static Account getRecord(String id) {
// Add your code
}
}
- Apex REST Walkthrough
- Next few steps walk you through the process of building an Apex REST service. Steps:
- Create Apex class that’s exposed as a REST service
- Try calling a few methods from a client
- Write unit tests
- The Apex class manages case records. Class contains five methods, and each method corresponds to an HTTP method
- Ex: when the client application invokes a REST call for the GET HTTP method, the
getCaseById
method is invoked - The class is defined with a URL mapping of
/Cases/*
, so the endpoint used to call this REST service is any URI that starts with:
https://yourInstance.my.salesforce.com/services/apexrest/Cases/
- Ex: when the client application invokes a REST call for the GET HTTP method, the
- Best practice is to consider versioning your API endpoints so that you can provide upgrades in functionality without breaking existing code:
- Ex: could create two classes specifying URL mappings of
/Cases/v1/*
and/Cases/v2/*
to implement this
- Ex: could create two classes specifying URL mappings of
- First step: create the following Apex REST class via: Dev Console > File > New > Apex Class
- Next few steps walk you through the process of building an Apex REST service. Steps:
CaseManager.apxc
@RestResource(urlMapping='/Cases/*')
global with sharing class CaseManager {
@HttpGet
global static Case getCaseById() {
RestRequest request = RestContext.request;
// grab the caseId from the end of the URL
String caseId = request.requestURI.substring(
request.requestURI.lastIndexOf('/')+1);
Case result = [SELECT CaseNumber,Subject,Status,Origin,Priority
FROM Case
WHERE Id = :caseId];
return result;
}
@HttpPost
global static ID createCase(String subject, String status,
String origin, String priority) {
Case thisCase = new Case(
Subject=subject,
Status=status,
Origin=origin,
Priority=priority);
insert thisCase;
return thisCase.Id;
}
@HttpDelete
global static void deleteCase() {
RestRequest request = RestContext.request;
String caseId = request.requestURI.substring(
request.requestURI.lastIndexOf('/')+1);
Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
delete thisCase;
}
@HttpPut
global static ID upsertCase(String subject, String status,
String origin, String priority, String id) {
Case thisCase = new Case(
Id=id,
Subject=subject,
Status=status,
Origin=origin,
Priority=priority);
// Match case by Id, if present.
// Otherwise, create new case.
upsert thisCase;
// Return the case ID.
return thisCase.Id;
}
@HttpPatch
global static ID updateCaseFields() {
RestRequest request = RestContext.request;
String caseId = request.requestURI.substring(
request.requestURI.lastIndexOf('/')+1);
Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
// Deserialize the JSON string into name-value pairs
Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(request.requestbody.tostring());
// Iterate through each parameter field and value
for(String fieldName : params.keySet()) {
// Set the field and value on the Case sObject
thisCase.put(fieldName, params.get(fieldName));
}
update thisCase;
return thisCase.Id;
}
}
- Create a Record with a Post method
- To invoke your REST service, you need a REST client. Example REST clients:
- Your own API client
- cURL command-line tool
- curl library for PHP
- Workbench tool
- Apex REST supports two formats for representations of resources: JSON and XML
- JSON is passed by default in the body of a request or response, format is indicated by the
Content-Type
property in the HTTP header - JSON is easier to read and understand than XML so it is used here to send a case record in JSON format
- JSON is passed by default in the body of a request or response, format is indicated by the
- Apex REST supports OAUTH 2.0 and session authentication mechanisms, which means Salesforce uses industry standards to keep your application and data safe
- You can use Workbench uses session authentication and can make testing easier
- Workbench is a powerful, web-based suite of tools for admins/developers to interact with orgs via Lightning Platform APIs
- To use Workbench to create records for this example
- Log into Salesforce. Then, go to https://workbench.developerforce.com/login.php
- Select Production
- Select your API version
- Accept the terms of service, the Login with Salesforce
- Click Allow to allow Workbench to access your information
- Enter your login credentials then Log in to Salesforce
- After logging in, select Utilities > REST Explorer
- Select POST
- URL path that the REST Explorer accepts is relative to the instance URL of the org. So, provide only the path that is appended to the instance URL. In this example, replace the default URI with
/services/apexrest/Cases/
- Add the JSON string representation of the object to insert in the code block below.
- Click Execute. This calls the method associated with the POST HTTP method, specifically the
createCase
method. - To view the response returned, click Show Raw Response, which includes the return value, in my case
500Dn000004iWchIAE
- You can use Workbench uses session authentication and can make testing easier
- To invoke your REST service, you need a REST client. Example REST clients:
{
"subject" : "Bigfoot Sighting!",
"status" : "New",
"origin" : "Phone",
"priority" : "Low"
}
- Retrieve Data with a Custom GET Method
- By following similar steps as before, use Workbench to invoke the GET HTTP method:
- In Workbench, select GET
- Enter the URI
/services/apexrest/Cases/<Record ID>
, replacing<Record ID>
with the ID in the record from the previous step - Click Execute. This calls the method associated with the GET HTTP method, namely
getCaseById
- By following similar steps as before, use Workbench to invoke the GET HTTP method:
- Retrive Data using cURL
- Every good developer should know how to use cURL
- cURL is a command-line tool for getting or sending files using URL syntax - it is useful for working with REST endpoints
- Instead of using Workbench for your Apex REST service, you can use cURL to invoke the GET HTTP method
- Note each time you “cURL” your REST endpoint, you pass the session ID for authorization. Workbench passes the session ID for you after you log in
- You also need a Security Token
- See the Trailhead for more details
- Use your Terminal (macOS/Linux) or Command Prompt (Windows) to execute the first code snippet below. If successful, it will return a result with an access_token, which is your session ID and instance_url for your org. It will look similar to the image below.
curl -v https://login.salesforce.com/services/oauth2/token -d "grant_type=password" -d "client_id=<your_consumer_key>" -d "client_secret=<your_consumer_secret>" -d "username=<your_username>" -d "password=<your_password_and_security_token>" -H "X-PrettyPrint:1"
- Next, enter your cURL command, which will be similar to the following to call your Apex REST service and return case info.
curl https://yourInstance.my.salesforce.com/services/apexrest/Cases/<Record_ID> -H "Authorization: Bearer <your_session_id>" -H "X-PrettyPrint:1"curl https://yourInstance.my.salesforce.com/services/apexrest/Cases/<Record_ID> -H "Authorization: Bearer <your_session_id>" -H "X-PrettyPrint:1"
- Every good developer should know how to use cURL
- Update Data with a Custom PUT or PATCH Method
- Update records with the PUT or PATCH HTTP methods
- PUT method is essentially an upsert: It either updates the entire resource, if it exists, or creates the resource if it doesn’t exist.
- PATCH method updates only the specified portions of an existing resource
- In Apex, update operations update only the specified fields and don’t overwrite the entire record
- The following sections include writing some Apex code to determine whether methods should update or upsert
- Update Data with the PUT Method
- The
upsertCase
method added to theCaseManager
class implements the PUT action - To invoke the PUT method:
- In Workbench REST Explorer, select PUT
- For the URI, enter
/services/apexrest/Cases/
- Add the following code snippet to the request body, and replace
<Record ID>
with the ID of the case record created earlier - Click Execute. This request invokes the
upsertCase
method, updating the Status, Subject, and Priority fields.- Note: the Status, Subject, and Priority fields are all updated. Since the request body didn’t contain a value for the Case Origin field, the origin parameter in the is null.
- The
- Update Data with the PATCH Method
- An alternative to the PUT method is the PATCH method - it is used to update record fields
- Multiple ways to use PATCH:
- Specify parameters in the method of each field to update. Ex: create a methdod to update the priority of a case with this signature
updateCasePriority(String priority)
. To update multiple fields, list all the desired fields as parameters. - Another approach is to pass the fields as JSON name/value pairs in the request body. This way, the method can accept an arbitrary number of parameters and the parameters aren’t fixed in the method’s signature.
updateCaseFields
method adaded to theCaseManager
class uses this approach. It deserializes the JSON string from the request body into a map of name/value pairs and uses the sObjectput
method to set the fields.
- Specify parameters in the method of each field to update. Ex: create a methdod to update the priority of a case with this signature
- To invoke the PATCH method:
- In Workbench REST Explorer, select PATCH
- Enter URI
/services/apexrest/Cases/<Record ID>
, where<Record ID>
should be replaced with the ID of the case record created earlier. Enter the JSON below in the Request Body. - Click Execute. This request invokes the
updateCaseFields
method in your REST service. The Status and Priority fields of the case record are updated to new values.
{
"status" : "Escalated",
"priority" : "High"
}
- Test Your Apex REST Class
- Testing an Apex REST class is similar to testing any other Apex class - just call the class methods by passing in parameter values and then verify the results
- For methods that don’t take parameters or that rely on information in the REST request, create a test REST request.
- To simulate a REST request, create a
RestRequest
in the test method, and then set properties on the request as follows. You can also add params that you “pass” in the request to simulate URI parameters
- To simulate a REST request, create a
// Set up a test request
RestRequest request = new RestRequest();
// Set request properties
request.requestUri =
'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
+ recordId;
request.httpMethod = 'GET';
// Set other properties, such as parameters
request.params.put('status', 'Working');
// more awesome code here....
// Finally, assign the request to RestContext if used
RestContext.request = request;
- If the method you’re testing accesses request values through
RestContext
, assign the request toRestContext
to populate it (RestContext.request = request;
)- To save the entire class and run the results:
- Dev Console > File > New > Apex Class, class name =
CaseManagerTest
then use the following - Save the class
- Run all the tests in your org via Test > Run All. The test code coverage for the
CaseManager
class is 100%.
- Dev Console > File > New > Apex Class, class name =
- To save the entire class and run the results:
@IsTest
private class CaseManagerTest {
@isTest static void testGetCaseById() {
Id recordId = createTestRecord();
// Set up a test request
RestRequest request = new RestRequest();
request.requestUri =
'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
+ recordId;
request.httpMethod = 'GET';
RestContext.request = request;
// Call the method to test
Case thisCase = CaseManager.getCaseById();
// Verify results
System.assert(thisCase != null);
System.assertEquals('Test record', thisCase.Subject);
}
@isTest static void testCreateCase() {
// Call the method to test
ID thisCaseId = CaseManager.createCase(
'Ferocious chipmunk', 'New', 'Phone', 'Low');
// Verify results
System.assert(thisCaseId != null);
Case thisCase = [SELECT Id,Subject FROM Case WHERE Id=:thisCaseId];
System.assert(thisCase != null);
System.assertEquals(thisCase.Subject, 'Ferocious chipmunk');
}
@isTest static void testDeleteCase() {
Id recordId = createTestRecord();
// Set up a test request
RestRequest request = new RestRequest();
request.requestUri =
'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
+ recordId;
request.httpMethod = 'DELETE';
RestContext.request = request;
// Call the method to test
CaseManager.deleteCase();
// Verify record is deleted
List<Case> cases = [SELECT Id FROM Case WHERE Id=:recordId];
System.assert(cases.size() == 0);
}
@isTest static void testUpsertCase() {
// 1. Insert new record
ID case1Id = CaseManager.upsertCase(
'Ferocious chipmunk', 'New', 'Phone', 'Low', null);
// Verify new record was created
System.assert(Case1Id != null);
Case case1 = [SELECT Id,Subject FROM Case WHERE Id=:case1Id];
System.assert(case1 != null);
System.assertEquals(case1.Subject, 'Ferocious chipmunk');
// 2. Update status of existing record to Working
ID case2Id = CaseManager.upsertCase(
'Ferocious chipmunk', 'Working', 'Phone', 'Low', case1Id);
// Verify record was updated
System.assertEquals(case1Id, case2Id);
Case case2 = [SELECT Id,Status FROM Case WHERE Id=:case2Id];
System.assert(case2 != null);
System.assertEquals(case2.Status, 'Working');
}
@isTest static void testUpdateCaseFields() {
Id recordId = createTestRecord();
RestRequest request = new RestRequest();
request.requestUri =
'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
+ recordId;
request.httpMethod = 'PATCH';
request.addHeader('Content-Type', 'application/json');
request.requestBody = Blob.valueOf('{"status": "Working"}');
RestContext.request = request;
// Update status of existing record to Working
ID thisCaseId = CaseManager.updateCaseFields();
// Verify record was updated
System.assert(thisCaseId != null);
Case thisCase = [SELECT Id,Status FROM Case WHERE Id=:thisCaseId];
System.assert(thisCase != null);
System.assertEquals(thisCase.Status, 'Working');
}
// Helper method
static Id createTestRecord() {
// Create test record
Case caseTest = new Case(
Subject='Test record',
Status='New',
Origin='Phone',
Priority='Medium');
insert caseTest;
return caseTest.Id;
}
}
- The following worked example is an Apex REST class that is accessible at
/Accounts/<Account_ID>/contacts
The services returns the account’s ID and name plus the ID and name of all contacts associated with the account.- The test class that follows achieves 100% code coverage for the class
AccountManager.apxc
@RestResource(urlMapping='/Accounts/*/contacts')
global with sharing class AccountManager{
@HttpGet
global static Account getAccount(){
RestRequest request = RestContext.request;
String accountId = request.requestURI.substringBetween('Accounts/','/contacts');
system.debug(accountId);
Account objAccount = [SELECT Id,Name,(SELECT Id,Name FROM Contacts) FROM Account WHERE Id = :accountId LIMIT 1];
return objAccount;
}
}
AccountManagerTest.apxc
//Test class
@isTest
private class AccountManagerTest{
static testMethod void testMethod1(){
Account objAccount = new Account(Name = 'test Account');
insert objAccount;
Contact objContact = new Contact(LastName = 'test Contact',
AccountId = objAccount.Id);
insert objContact;
Id recordId = objAccount.Id;
RestRequest request = new RestRequest();
request.requestUri =
'https://resourceful-narwhal-v8vop4-dev-ed.my.salesforce.com/services/apexrest/Accounts/'
+ recordId +'/contacts';
request.httpMethod = 'GET';
RestContext.request = request;
// Call the method to test
Account thisAccount = AccountManager.getAccount();
// Verify results
System.assert(thisAccount!= null);
System.assertEquals('test Account', thisAccount.Name);
}
}