Skip to main content

Apex Web Services

Learning Objectives

After completing this module, you'll be able to:

  • 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.

Follow Along with Trail Together

Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series on Trailhead Live. 

(This clip starts at the 1:07:03 minute mark, in case you want to rewind and watch the beginning of the step again.)

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 your methods callable through the web, your external applications can integrate with Salesforce to perform all sorts of nifty operations.

For example, say your company's call center is using an internal application to manage on-premises resources. Customer support representatives are expected to use the same application to perform their daily work, including managing case records in Salesforce. By using one interface, representatives can view and update case records and access internal resources. The application calls an Apex web service class to manage Salesforce case records.

Expose a Class as a REST Service

Making your Apex class available as a REST web service is straightforward. Define your class as global, and define methods as global static. Add annotations to the class and methods. For example, this sample Apex REST class uses one method. The getRecord method is a custom REST API call. It's annotated with @HttpGet and is invoked for a GET request.

@RestResource(urlMapping='/Account/*')

global with sharing class MyRestResource {
@HttpGet
global static Account getRecord() {
// Add your code
}
}

As you can see, the class is annotated with @RestResource(urlMapping='/Account/*'). The base endpoint for Apex REST is https://yourInstance.my.salesforce.com/services/apexrest/. The URL mapping is appended to the base endpoint to form the endpoint for your REST service. For example, in the class example, the REST endpoint is https://yourInstance.my.salesforce.com/services/apexrest/. For your org, it could look something like, https://yourInstance.my.salesforce.com/services/apexrest/Account/*.

Note

In this module when you see “yourInstance” in links or code be sure to change it out with your Salesforce Org instance.

The URL mapping is case-sensitive and can contain a wildcard character (*).

Define each exposed method as global static and add an annotation to associate it with an HTTP method. The following annotations are available. You can use each annotation only once in each Apex class.

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.

Expose a Class as a SOAP Service

Making your Apex class available as a SOAP web service is as easy as with REST. Define your class as global. Add the webservice keyword and the static definition modifier to each method you want to expose. The webservice keyword provides global access to the method it is added to.

For example, here's a sample class with one method. The getRecord method is a custom SOAP API call that returns an Account record.

global with sharing class MySOAPWebService {

webservice static Account getRecord(String id) {
// Add your code
}
}

The external application can call your custom Apex methods as web service operations by consuming the class WSDL file. Generate this WSDL for your class from the class detail page, accessed from the Apex Classes page in Setup. You typically send the WSDL file to third-party developers (or use it yourself) to write integrations for your web service.

Because platform security is a first-class Salesforce citizen, your web service requires authentication. In addition to the Apex class WSDL, external applications must use either the Enterprise WSDL or the Partner WSDL for login functionality.

Apex REST Walkthrough

Now the fun stuff. The next few steps walk you through the process of building an Apex REST service. First, you create the Apex class that is exposed as a REST service. Then you try calling a few methods from a client, and finally write unit tests. There's quite a bit of code, but it will be worth the effort!

Your Apex class manages case records. The class contains five methods, and each method corresponds to an HTTP method. For example, when the client application invokes a REST call for the GET HTTP method, the getCaseById method is invoked.

Because the class is defined with a URL mapping of /Cases/*, the endpoint used to call this REST service is any URI that starts with https://yourInstance.my.salesforce.com/services/apexrest/Cases/.

We suggest that you also think about versioning your API endpoints so that you can provide upgrades in functionality without breaking existing code. You could create two classes specifying URL mappings of /Cases/v1/* and /Cases/v2/* to implement this functionality.

Let's get started by creating an Apex REST class.

  1. Open the Developer Console from Setup (Setup).
  2. In the Developer Console, select File | New | Apex Class.
  3. For the class name, enter CaseManager and then click OK.
  4. Replace the autogenerated code with the following class definition.
    @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;
    }
    }

  5. Press CTRL+S to save.

Create a Record with a POST Method

Let's use the Apex REST class that you've just created and have some fun. First, we'll call the POST method to create a case record.

To invoke your REST service, you need to use a… REST client! You can use almost any REST client, such as your own API client, the cURL command-line tool, or the curl library for PHP. We'll use the Workbench tool as our REST client application, but we'll take a peek at cURL later on.

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. Since JSON is easier to read and understand than XML, this unit uses JSON exclusively. In this step, you send a case record in JSON format.

Apex REST supports OAuth 2.0 and session authentication mechanisms. In simple terms, this means that we use industry standards to keep your application and data safe. Fortunately, you can use Workbench to make testing easier. Workbench is a powerful, web-based suite of tools for administrators and developers to interact with orgs via Lightning Platform APIs. With Workbench, you use session authentication as you log in with your username and password to Salesforce. And you use the REST Explorer to call your REST service.

  1. Navigate to https://workbench.developerforce.com/login.php.
  2. For Environment, select Production.
  3. Select the latest API version from the API Version drop-down.
    Note

    You may need to select a different API version if the latest version has issues.

  4. Accept the terms of service, and click Login with Salesforce.
  5. To allow Workbench to access your information, click Allow.
  6. Enter your login credentials and then click Log in to Salesforce.
  7. After logging in, select utilities | REST Explorer.
  8. Select POST.
  9. The URL path that REST Explorer accepts is relative to the instance URL of your org. Provide only the path that is appended to the instance URL. In the relative URI input field, replace the default URI with /services/apexrest/Cases/.
  10. For the request body, insert the following JSON string representation of the object to insert.
    {
    
    "subject" : "Bigfoot Sighting!",
    "status" : "New",
    "origin" : "Phone",
    "priority" : "Low"
    }

  11. Click Execute.
    This invocation calls the method that is associated with the POST HTTP method, namely the createCase method.
  12. To view the response returned, click Show Raw Response.
    The returned response looks similar to this response. The response contains the ID of the new case record. Your ID value is likely different from 50061000000t7kYAAQ. Save your ID value to use in the next steps.
    HTTP/1.1 200 OK
    
    Date: Wed, 07 Oct 2015 14:18:20 GMT
    Set-Cookie: BrowserId=F1wxIhHPQHCXp6wrvqToXA;Path=/;Domain=.salesforce.com;Expires=Sun, 06-Dec-2015 14:18:20 GMT
    Expires: Thu, 01 Jan 1970 00:00:00 GMT
    Content-Type: application/json;charset=UTF-8
    Content-Encoding: gzip
    Transfer-Encoding: chunked
    "50061000000t7kYAAQ"

Retrieve Data with a Custom GET Method

By following similar steps as before, use Workbench to invoke the GET HTTP method.

  1. In Workbench, select GET.
  2. Enter the URI /services/apexrest/Cases/<Record ID>, replacing <Record ID> with the ID of the record you created in the previous step.
  3. Click Execute. This invocation calls the method associated with the GET HTTP method, namely the getCaseById method.
  4. To view the response returned, click Show Raw Response.
    The returned response looks similar to this response. The response contains the fields that the method queried for the new case record.
    HTTP/1.1 200 OK
    
    Date: Wed, 07 Oct 2015 14:28:20 GMT
    Set-Cookie: BrowserId=j5qAnPDdRxSu8eHGqaRVLQ;Path=/;Domain=.salesforce.com;Expires=Sun, 06-Dec-2015 14:28:20 GMT
    Expires: Thu, 01 Jan 1970 00:00:00 GMT
    Content-Type: application/json;charset=UTF-8
    Content-Encoding: gzip
    Transfer-Encoding: chunked
    {
    "attributes" : {
    "type" : "Case",
    "url" : "/services/data/v34.0/sobjects/Case/50061000000t7kYAAQ"
    },
    "CaseNumber" : "00001026",
    "Subject" : "Bigfoot Sighting!",
    "Status" : "New",
    "Origin" : "Phone",
    "Priority" : "Low",
    "Id" : "50061000000t7kYAAQ"
    }

Update Data with a Custom PUT or PATCH Method

You can update records with the PUT or PATCH HTTP methods. The PUT method either updates the entire resource, if it exists, or creates the resource if it doesn't exist. PUT is essentially an upsert method. The 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. We'll write some Apex code to determine whether our methods update or upsert.

Update Data with the PUT Method

The upsertCase method that you added to the CaseManager class implements the PUT action. This method is included here for your reference. The method uses the built-in upsert Apex DML method to either create or overwrite case record fields by matching the ID value. If an ID is sent in the body of the request, the case sObject is populated with it. Otherwise, the case sObject is created without an ID. The upsert method is invoked with the populated case sObject, and the DML statement does the rest. Voila!

@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;
}

To invoke the PUT method:

  1. In Workbench REST Explorer, select PUT.
  2. For the URI, enter /services/apexrest/Cases/.
  3. The upsertCase method expects the field values to be passed in the request body. Add the following for the request body, and then replace <Record ID> with the ID of the case record you created earlier.
    {
    
    "id": "<Record_ID>",
    "status" : "Working",
    "subject" : "Bigfoot Sighting!",
    "priority" : "Medium"
    }

    Note

    The ID field is optional. To create a case record, omit this field. In our example, you're passing this field because you want to update the case record.

  4. Click Execute.
    This request invokes the upsertCase method from your REST service. The Status, Subject, and Priority fields are updated. The subject is updated, even though its value matches the old subject. Also, because the request body didn't contain a value for the Case Origin field, the origin parameter in the upsertCase method is null. As a result, when the record is updated, the Origin field is cleared. To check these fields, view this record in Salesforce by navigating to https://yourInstance.my.salesforce.com/<Record ID>.

Update Data with the PATCH Method

As an alternative to the PUT method, use the PATCH method to update record fields. You can implement the PATCH method in different ways. One way is to specify parameters in the method for each field to update. For example, you can create a method to update the priority of a case with this signature: updateCasePriority(String priority). To update multiple fields, you can list all the desired fields as parameters.

Another approach that provides more flexibility is to pass the fields as JSON name/value pairs in the request body. That way the method can accept an arbitrary number of parameters, and the parameters aren't fixed in the method's signature. Another advantage of this approach is that no field is accidentally cleared because of not being included in the JSON string. The updateCaseFields method that you added to the CaseManager class uses this second approach. This method deserialize the JSON string from the request body into a map of name/value pairs and uses the sObject put method to set the fields.

@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;
}

To invoke the PATCH method:

  1. In Workbench REST Explorer, click PATCH.
  2. For the URI, enter /services/apexrest/Cases/<Record ID>. Replace <Record ID> with the ID of the case record created earlier. Enter the following JSON in the Request Body.
    {
    
    "status" : "Escalated",
    "priority" : "High"
    }

    This JSON has two field values: status and priority. The updateCaseFields method retrieves these values from the submitted JSON and are used to specify the fields to update in the object.
  3. 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. To check these fields, view this record in Salesforce by navigating to https://yourInstance.my.salesforce.com/<Record ID>.

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.

In general, here's how you test Apex REST services. 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.

// 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 to RestContext to populate it (RestContext.request = request;).

Now, let's save the entire class in the Developer Console and run the results.

  1. In the Developer Console, select File | New | Apex Class.
  2. For the class name, enter CaseManagerTest and then click OK.
  3. Replace the autogenerated code with the following class definition.
    @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;
    }
    }

  4. Press CTRL+S to save.
  5. Run all the tests in your org by selecting Test | Run All.

The test results display in the Tests tab. After the test execution finishes, check the CaseManager row in the Overall Code Coverage pane. It's at 100% coverage.

Tell Me More ...

Learn about supported data types and namespaces in Apex REST, Salesforce APIs, and security considerations.

Supported Data Types for Apex REST

Apex REST supports these data types for parameters and return values.

  • Apex primitives (excluding Object 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.

Namespaces in Apex REST Endpoints

Apex REST methods can be used in managed and unmanaged packages. 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. For example, if the class is contained in a managed package namespace called packageNamespace and the Apex REST methods use a URL mapping of /MyMethod/*, the URL used via REST to call these methods would be of the form https://instance.my.salesforce.com/services/apexrest/packageNamespace/MyMethod/.

Custom Apex Web Services and Salesforce APIs

Instead of using custom Apex code for REST and SOAP services, external applications can integrate with Salesforce by using Salesforce's REST and SOAP APIs. These APIs let you create, update, and delete records. However, the advantage of using Apex web services is that Apex methods can encapsulate complex logic. This logic is hidden from the consuming application. Also, the Apex class operations can be faster than making individual API calls, because fewer roundtrips are performed between the client and the Salesforce servers. With an Apex web service call, there is only one request sent, and all operations within the method are performed on the server.

Security Considerations for Apex Web Services

The security context under which Apex web service methods run differs from the security context of Salesforce APIs. Unlike Salesforce APIs, Apex web service methods run with system privileges and don't respect the user's object and field permissions. However, Apex web service methods enforce sharing rules when declared with the with sharing keyword.

Resources

Keep learning for
free!
Sign up for an account to continue.
What’s in it for you?
  • Get personalized recommendations for your career goals
  • Practice your skills with hands-on challenges and quizzes
  • Track and share your progress with employers
  • Connect to mentorship and career opportunities