img1

Learning Objectives

After completing this article, you’ll be able to:

Prologue

Ahoy, today’s topic will be on exposing your Apex classes and methods so that external applications can access your application. By making your methods callable through the web, your external applications can integrate with Salesforce to perform all sorts of nifty operations.

Expose a Class as a REST Service

Making APEX classes REST exposable is very straightforward. Simply define your class as global, and define methods as global static. The next step is to annotate each method conforming to a HTTP verb (GET, POST, etc). For example, the code illustrates the correct way to retrieve a Case resource. it’s annotated with @HttpGet and is invoked for a GET request.

1
2
3
4
5
6
7
@RestResouce(urlMapping='/Case/*')
global with sharing class SampleRestResource {
    @HttpGet
    global static Case getCaseRecord(){

    }
}

The above code snippet wasn’t too hard to understand right? he base endpoint for Apex REST is https://yourInstance.salesforce.com/services/apexrest/. he URL mapping is appended to the base endpoint to form the endpoint for your REST service https://yourInstance.salesforce.com/services/apexrest/Case.

Apex REST Annotations

AnnotationActionInfo
@HttpGetReadRead records
@HttpPostCreateCreate records
@HttpPutUpsertUpdate if existing else create records
@HttpDeleteDeleteDelete records
@HttpPatchUpdateUpdate exiting records

Governor Limits

Calls to Apex REST classes count against the organization’s API governor limits. the maximum request or response size is 6 MB for synchronous Apex or 12 MB for asynchronous Apex.

Methods annotated with @HttpGet or @HttpDelete should have no parameters. This is because GET and DELETE requests have no request body, so there’s nothing to deserialize.

Apex REST Methods Versioning

it is recommended to implement an versioning strategy for your API endpoints so that you can provide upgrades in functionality without breaking existing code. For example, /Account/v1/* and /Lead/v2/*.

Apex REST Methods

Apex REST supports two formats for representations of resources: JSON and XML. JSON representations are passed by default in the body of a request or response, and the format is indicated by the Content-Type property in the HTTP header.

A single Apex class annotated with @RestResource can’t have multiple methods annotated with the same HTTP request method. For example, the same class can’t have two methods annotated with @HttpGet.

Apex REST Methods Parameters

You can retrieve the body as a Blob from the HttpRequest object if there are no parameters to the Apex method. If parameters are defined in the Apex method, an attempt is made to deserialize the request body into those parameters. If the Apex method has a non-void return type, the resource representation is serialized into the response body. The following return and parameter types are allowed:

  • Apex primitives (excluding sObject and Blob)
  • sObjects
  • Lists or maps of Apex primitives or sObjects (only maps with String keys are supported)
  • User-defined types that contain member variables of the types listed above

Apex REST currently doesn’t support requests of Content-Type multipart/form-data. RestRequest and RestResponse objects are available by default in your Apex methods through the static RestContext object. This example shows how to access these objects through RestContext:

1
2
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;

Apex REST Method Considerations

Here are a few points to consider when you define Apex REST methods:

  • If a login call is made from the API for a user with an expired or temporary password, subsequent API calls to custom Apex REST Web service methods aren’t supported and result in the MUTUAL_AUTHENTICATION_FAILED error
  • When calling Apex REST methods that are contained in a managed package, you need to include the managed package namespace in the REST call URL
  • An Apex method with a non-void return type will have the return value serialized into RestResponse.responseBody
  • If the Apex method has no parameters, Apex REST copies the HTTP request body into the RestRequest.requestBody property. If the method has parameters, then Apex REST attempts to deserialize the data into those parameters and the data won’t be deserialized into the RestRequest.requestBody property

REST Parameter User-Defined Types

You can use user-defined types for parameters in your Apex REST methods. Apex REST deserializes request data into public, private, or global class member variables of the user-defined type, unless the variable is declared as static or transient. Also please remember members variables must types allowed by APEX rest as described above, Example below: \* is used to escape the * for demonstration purposes, not needed in actual APEX.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@RestResouce(urlMapping='/Opportunity/\*')
global with sharing RestParameterExample {
    @HttpPost
    global static DefinedType createRecord(DefinedType record) {
        return record;
    }
    
    global class DefinedType {
        /* accepted */
        global String name;
        private Decimal height;
        public String description;
        /* accepted */
        global transient String random1; //ignored
        global static String random2; //ignored
    }
}

Valid JSON request data for this method would look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* If a value for random1 or random2 is provided in the example request data above,
   an HTTP 400 status code response is generated. 
*/
{
"record": {
    "name": "Awesome Reader",
    "height": 5.9,
    "description": "Learning new resources"
}
}

Request and Response Data Considerations

  • The URL patterns testPattern and testPattern/* match the same URL. An REST request for this URL pattern resolves to the class that was saved first
  • For request data in either JSON or XML, valid values for Boolean parameters are: true, false (both of these are treated as case-insensitive), 1 and 0 (the numeric values, not strings of “1” or “0”). Any other values for Boolean parameters result in an error
  • If the JSON or XML request data contains multiple parameters of the same name, this results in an HTTP 400 status code error response
  • Some parameter and return types can’t be used with XML as the Content-Type for the request or as the accepted format for the response, and hence, methods with these parameter or return types can’t be used with XML
    • Lists, maps, or collections of collections, for example, List> aren’t supported ( not applicable to JSON)
    • If the parameter list includes a type that’s invalid for XML and XML is sent, an HTTP 415 status code is returned
    • If the return type is a type that’s invalid for XML and XML is the requested response format, an HTTP 406 status code is returned

Security Considerations

Invoking a custom Apex REST Web service method always uses system context. Consequently, the current user’s credentials are not used, and any user who has access to these methods can use their full power, regardless of permissions, field-level security, or sharing rules. Developers who expose methods using the Apex REST annotations should therefore take care that they are not inadvertently exposing any sensitive data. Apex class methods that are exposed through the Apex REST API don’t enforce object permissions and field-level security by default. Use the following to align with best practices:

  • To enforce object or field-level security while using SOQL SELECT statements in Apex, use the WITH SECURITY_ENFORCED clause
  • You can strip user-inaccessible fields from query and subquery results, or remove inaccessible sObject fields before DML operations, by using the Security.stripInaccessible method
  • Declare classes with the with sharing keyword, this in turns enables record-level access enforcement

Apex REST Guidance

To invoke your APEX REST service, you need to use a REST client. You can use almost any REST client, such as your own API client, the examples going forward will utilize the cURL command-line tool. Each time you invoke a request, you should pass along the session ID for authorization. To obtain a session ID, you first create a connected app in your Salesforce organization and enable OAuth. Your client application, cURL in this case, uses the connected app to connect to Salesforce. Follow these instructions to create a connected app that provides you with the consumer key and consumer secret that you need to get your session ID.

Session ID Retrieval

To get your sessionId, use the following command. A SOAP API login() call returns the session ID. You can also have the session ID. The password actually is an combination from login password and Salesforce security token. To retrieve your security, that’s if you have stored somewhere place 😃 read up here

  • Navigate to My Personal Information
  • Reset My Security Token
  • Reset your token (sent by email)
  • Use your token

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'

After you’ve successfully authenticated you should see text resembling the following. The result includes an access_token, which is your session ID and instance_url for your organization.

img2

Sample Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@RestResource(urlMapping='/Cases/*')
global with sharing class FeatureCaseManager {
    @HttpGet
    global static Case getCaseById(){
        RestRequest req = RestContext.request;
        String uri = req.requestURI;
        // get the case id
        String recordId = uri.substring(uri.lastIndexOf('/')+1);
        Case result =  [SELECT CaseNumber,Subject,Status,Origin,Priority
                        FROM Case
                        WHERE Id = :recordId];
        return result;
    }
    @HttpPost
    global static ID createCase(String subject, String status, String origin, String priority) {
        Case record = new Case(
            Subject=subject,
            Status=status,
            Origin=origin,
            Priority=priority);
        insert record;
        return record.Id;
    }
    @HttpDelete
    global static void deleteCase(){
        RestRequest req = RestContext.request;
        String uri = req.requestUri;
        // get the case id
        String recordId = uri.substring(uri.lastIndexOf('/')+1);
        Case record = [SELECT Id FROM Case WHERE Id = :recordId];
        delete record;
    }
    @HttpPut
    global static ID upsertCase(String subject, String status, String origin, String priority, String id){
        Case record = new Case(
                Id=id,
                Subject=subject,
                Status=status,
                Origin=origin,
                Priority=priority);
        // Match case by Id, if present.
        // Otherwise, create new case.
        upsert record;
        // Return the case ID.
        return record.Id;
    }
    @HttpPatch
    global static ID updateCase(){
        RestRequest req = RestContext.request;
        String uri = req.requestUri;
        Blob body = req.requestbody;
        // get the case id
        String recordId = uri.substring(uri.lastIndexOf('/')+1);
        Case record = [SELECT Id FROM Case WHERE Id = :recordId];
        // Deserialize the JSON string into name-value pairs
        Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(body.tostring());
        // Iterate through each parameter field and value
        for(String fieldName : params.keySet()) {
            // Set the field and value on the Case sObject
            record.put(fieldName, params.get(fieldName));
        }
        update record;
        return record.Id;
    }
} 

The interesting section is finally about to start, we will not try executing each of the rest endpoints created earlier using cURL. Use the following commands to access your endpoints accordingly.

Create Data (POST)

The following snippet create a new Case record with the following information, the newly created case id should be in the response:

1
2
3
4
curl https://salesforceinstance.com/services/apexrest/Cases -X POST 
-d '{"subject":"Testing", "status":"New","origin":"Phone","priority":"low"}' 
-H 'Authorization: Bearer <access-token>' 
-H 'X-PrettyPrint:1' -H "Content-Type: application/json"

Retrieve Data (GET)

Use the returned id to query the created case record:

curl https://salesforceinstance.com/services/apexrest/Cases/<record_id> -H 'Authorization: Bearer <access_token>' -H 'X-PrettyPrint:1'

img3

Test Your Apex REST Class

Testing your 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// the following are intended to be simple and straight forward, it is by no means fully fleshed out.
@isTest
public class TestFactory {
    public static Id CreateCaseRecord() {
        // Create test record
        Case record = new Case(
            Subject='Test Case',
            Status='New',
            Origin='Phone',
            Priority='High');
        insert record;
        return record.Id;
    }   
}
@isTest
public class FeatureCaseManagerTest {
    @isTest static void testGetCaseById() {
        ID recordId = TestFactory.CreateCaseRecord();
        RestRequest req = new RestRequest();
        req.requestURI = 'https://yourInstance.salesforce.com/services/apexrest/Cases/'+ recordId;
        req.httpMethod = 'GET';
        RestContext.request = req;
        // Call the method to test
        Case record = FeatureCaseManager.getCaseById();
        // Assert results
        System.assert(record != null);
        System.assertEquals('High', record.Priority);
    }
    @isTest static void testCreateCase() {
        ID recordId = FeatureCaseManager.createCase('Chivalry Strives', 'New', 'Phone', 'Medium');
        // Assert results
        System.assert(recordId != null);
        Case thisCase = [SELECT Id,Subject FROM Case WHERE Id=:recordId];
        System.assert(thisCase != null);
        System.assertEquals(thisCase.Subject, 'Chivalry Strives');
    }
    @isTest static void testDeleteCase() {
        ID recordId = TestFactory.CreateCaseRecord();
        // Set up a test request
        RestRequest req	 = new RestRequest();
        req.requestUri ='https://yourInstance.salesforce.com/services/apexrest/Cases/'+ recordId;
        req.httpMethod = 'GET';
        RestContext.request = req;
        // Call the method to test
        FeatureCaseManager.deleteCase();
        // Assert 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 = FeatureCaseManager.upsertCase('Chivalry Strives', 'New', 'Web', 'Medium', null);
        // Assert 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, 'Chivalry Strives');
        // 2. Update status of existing record to Working
        ID case2Id = FeatureCaseManager.upsertCase('Chivalry Strives', 'Working', 'Phone', 'Low', case1Id);
        // Assert 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 testUpdateCase() {
        Id recordId = TestFactory.CreateCaseRecord();
        RestRequest req = new RestRequest();
        req.requestUri ='https://yourInstance.salesforce.com/services/apexrest/Cases/'+ recordId;
        req.httpMethod = 'PATCH';
        req.addHeader('Content-Type', 'application/json');
        req.requestBody = Blob.valueOf('{"status": "Working"}');
        RestContext.request = req;
        // Update status of existing record to Working
        ID recordId2 = FeatureCaseManager.updateCase();
        // Assert record was updated
        System.assert(recordId2 != null);
        Case record = [SELECT Id,Status FROM Case WHERE Id=:recordId2];
        System.assert(record != null);
        System.assertEquals(record.Status, 'Working');
    }  
}

Epilogue

Phew 😆, that was quite the restful journey, we learned quite a lot, starting with Rest Resources in APEX. This includes the various different Http verbs used to create, update and delete resources. We also explored best practices, security considerations, general considerations and sample code samples to get your feet wet. Thanks for stopping by and I hope you were able to learn or reinforce your knowledge regarding Rest Resources on the Salesforce platform 😃