Sunday 19 January 2014

Creating A Salesforce Rest API

This article is specific to using Salesforce REST between servers for integration purposes.  Specifically for an enterprise server to get data to and from Salesforce.  The benefits include real time data using your business rules, the REST service can take advantage of a service layer created in apex that includes business logic, the rest service can be used by enterprise applications, mobile applications etc all sharing common business rules.
Overview:
1. Create a connected app in Salesforce.
2. Create a Global class used for REST requests
3. Test out the connection using Curl or Postman in Google chrome.
Detail:
The first step is to create a connected app in Salesforce. 
Go to Setup/App Setup/Create/Apps and scroll down to Connected Apps. Click the new button.
Give it a name.  (The name can be anything.  It is NOT used to call the service)
The API name will be created automatically or you can set it to something else. (again the api name is NOT used for calling the service)
Enter your contact email and phone number.
The logo image url and icon url is not required for our server to server communication.  The icons are used for two of the oauth2 authentication flows that expose the login screen to the user to enter their salesforce credentials.  Since this is server to server the logos are not required.
Check Enable OAuth Settings.
For server to server the callback url is not used but you have to enter something.  You can set it to anything like https://test.salesforce.com/services/oauth2/token.  Please note this example is using a sandbox for setup.  For production use https://login.salesforce.com/services/oauth2/token.
Leave use digital signatures unchecked.
For OAuth Scopes choose Access and manage your data (api).
Click Save.
A consumer key and consumer secret will be created.  These two pieces of information is required to authenticate using oauth2. 
I am glossing over oauth2 which is an interesting subject.  I am just going to say that Oauth2 has 3 authentication flows that are used most often.  Two of them offer a login screen to the user to login to Salesforce for authentication and then pass control back to the calling application that is accessing Salesforce data.  This can be a web server application or a mobile application.  It is using the users authentication and approval to use the users Salesforce data in the calling application.  The third flow which is the one we are using does not pass control to the user.  It requires the server that is logging into Salesforce to pass the login and password to the server.  This has a security concern.  The password is accessible to the server so must be kept securely so others cannot access it.  The security token needs to be appended to the password.  The security token is what gets emailed to you when the password is changed in Salesforce.  The security token should be hidden from users as well.  Since this is a server to server integration it is important that the password not expire. It is never a good when you come to work and find the integration has stopped working.  The profile that this service user uses should be set to password never expires.
Lets set up the Apex class for the service.  The ficticious class below is going to get a quote created in Salesforce.
The comment before the line describes what the line does so
// This is used to call the service. The complete request would look like "https://cs3.salesforce.com/services/apexrest/v1/quote/a0JQ00000000000" 
Notice the v1/quote defined in line 1.  I rev the rest request in case changes are done that may break the current version.  You can create another class with a new version allowing code written elsewhere to continue to function.  Notice also the Id of the record appended.  The method after the @HttpGet looks for the id and uses it to query the record. 
1 @RestResource(urlMapping='/v1/quote/*') 
2 global class RestQuote {
// This tells the compiler to run this method on an http get request
3 @HttpGet 
// I return a quote from Salesforce
4    global static QuoteData getquote(){
5        RestRequest req = RestContext.request; 
// This line extracts the id passed in the http request.
6        String QuoteId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1); 
// the next line could be and should be a call to a service layer class called quote that handles the business rules around quote.  I have it calling a method below.
7        QuoteData qd = getQuoteById(quoteId);
8        return qd;
    }
  
9    private static QuoteData getQuoteById (String id){
10        QuoteData od = null;
11        List<Quote__c> quote = null;
12        try{ 
// I return a List instead of a single record.  I find it better for trapping errors, beyond the scope of this article.
13            quote = [Select Name, Account, .... from Quote__c where Id=:id];
        }
14        catch(Exception e){
15            String error = e.getMessage();
16            qd = new QuoteData(error);
17            return qd;
        }
18        if (quote.size() == 0){
19            qd = new QuoteData('There was no quote found');
        }
        else {
20            Quote__c myQuote = quote[0]; 
// I am doing this to show that a class can be used to format the data before being returned.
21            qd = new QuoteData(myQuote.Name, myQuote.Account, ....);
        }
22        return od;
    }
// I am an inner class that can be used to format the data add business rules etc.    
23    global class QuoteData{
24        String name = '';
25        String account = '';
26        String ... = '...';
27        String error = 'none';
      
28        global quoteData(String name, String account, ...){
29            this.name = name;
30            this.account = account;
31            this.... = ...;
        }
      
32        global quoteData(String error){
33            this.error = error;
        }
    }
}
This class could work however I would not write a class like this.  Replacing GetQuoteById() on line 7 with a call to a service layer quote class that contains the business rules would be much better.  This will allow the code to be reused in visualforce pages used in the Salesforce client, any site attached to the Salesforce instance and mobile apps used against the instance.    I include the QuoteData inner class starting on line 23 to show how you can format the data and send it in the Rest request.
It will be returned in a JSON packet that looks something like this:
{name:data, account:data, error:data}
You can also return XML
Let's make the request.  You can test out your REST request using curl.  There is a google chrome extension called postman that is easy to use as well which I show below. 
First authenticate:
https://test.salesforce.com/services/oauth2/token passing the following parameters
grant_type: set equal to password
client_id: which contains the consumer key created for you when creating the connected application above.
client_secret: holds the consumer secret created for you when creating the connected application above.
username: is the username of a salesforce license user
password: is the password for the salesforce license user.  You must append the security token to the password for the authentication to work.  The security token gets emailed to you when the password is reset.
Postman Test: 
Notice the authentication is a post request.
Postman Auth
The response will look like the following and include the access_token:
{
    "id": "https://test.salesforce.com/id/00DQ00000027rMJMAY/00580000003cONXAA2",
    "issued_at": "1380481775164",
    "instance_url": "https://cs3.salesforce.com",
    "signature": "ldsjsdlfjlsdfjlsdfjfldsdtnE0XCAYEogAFw4NfBxjHupo=",
    "access_token": "lkjflkdjflkdjflkdjfldsjflsdkfjY6yC5xU466yxh06zvKaO0xqQh42b4bFBhVkUO.xarhcT0ec8PVcBnv"
}
Use the access token in the next request:  https://cs3.salesforce.com/services/apexrest/v1quote/[id]
Note that the header key value pair is Authentication : Bearer [access token returned above]
RestQuote3

 The returned data would look something like this.
{
    "Name": "QT-00001",
    "Account": "ACME HARDWARE",
    "...": "...",
    "error": "none"
}

No comments:

Post a Comment