WebService - wimvelzeboer/fflib-apex-extensions GitHub Wiki

REST WebService

Contents

Simple Record Access

GET

For retrieving an entry

 +-------------------------------------------------+
 | Retrieve and sanitised request arguments / body |
 +-------------------------------------------------+
      |
      V
 +-----------------------+
 | Get the required data |
 +-----------------------+
       |
       V
 +---------------------------------------+
 | Translate, filter and return the data |
 +---------------------------------------+

Example

In the following example you see a very basic GET request to retrieve the Account Rating value for the provided AccountId. The request is send to Https://instanceURL.salesforce.com/services/apexrest/AccountRating/v1.0 and contains a body something like:

{
  "AccountIds" :
  [
    "001000000000001AAA",
    "001000000000002AAA"
  ]
}
Expecting a response with the Ids and their Rating value:
{
  "accountRatings" :
  [
    { "AccountId" : "001000000000001AAA", "Rating" : "Warm" },
    { "AccountId" : "001000000000002AAA", "Rating" : "Hot" }
  ]
}
As you can see in the example below most of the code that is written is validating the security, converting the request and response bodies and a proper error handling.

As we are dealing with a simple data retrieval from the database, a direct call to the domain is made to retrieve the records. If data comes from multiple objects, we can use multiple domains/selector classes.

When the webservice is supposed to invoke a business process, you should call a service method here.

@RestResource(UrlMapping='/AccountRating/v1.0')
global with sharing class AccountRatingWebService
{
    private static final SObjectType ACCOUNT_SOBJECT_TYPE = Schema.Account.SObjectType;
    private static final String CRUD_ERROR_MESSAGE = 'Not Authorised to access Accounts';
    private static final String FLS_ERROR_MESSAGE = 'Not Authorised to access Rating data';
    private static final String INVALID_FORMATTED_REQUEST = 'Invalid formatted request';
    private static final String UNHANDLED_EXCEPTION_OCCURRED = 'Unhandled exception occurred';

    /**
     * Gets the account rating for the given account Id
     */
    @HttpGet
    global static void doGet()
    {
        try
        {
            // Retrieve sanitised request body
            GetRequestBody request = getGetRequestBody();

            // Validate that the user has the proper access
            fflib_SecurityUtils.checkObjectIsReadable(ACCOUNT_SOBJECT_TYPE);
            fflib_SecurityUtils.checkFieldIsReadable(ACCOUNT_SOBJECT_TYPE, Account.Rating);

            // Get the required data
            IAccounts accounts = Accounts.newInstance(request.AccountIds);

            // Translate, filter and return the data
            RestContext.response.responseBody = new GetResponseBody(accounts).toBlob();
            RestContext.response.statusCode = 200;
        }
        // Unauthorised to SObject
        catch (fflib_SecurityUtils.CrudException e)
        {
            RestContext.response.statusCode = 401;
            RestContext.response.responseBody =
                    new ErrorResponse(CRUD_ERROR_MESSAGE).toBlob();
        }
        // Unauthorised to Field
        catch (fflib_SecurityUtils.FlsException e)
        {
            RestContext.response.statusCode = 401;
            RestContext.response.responseBody =
                    new ErrorResponse(FLS_ERROR_MESSAGE).toBlob();
        }
        // JSON formatting error
        catch (System.JSONException e)
        {
            RestContext.response.statusCode = 400;
            RestContext.response.responseBody =
                    new ErrorResponse(INVALID_FORMATTED_REQUEST).toBlob();
        }
        catch (Exception e)
        {
            RestContext.response.statusCode = 400;
            RestContext.response.responseBody =
                    new ErrorResponse(UNHANDLED_EXCEPTION_OCCURRED).toBlob();
        }
    }

    public class GetRequestBody
    {
        public Set<Id> AccountIds { get; set; }
    }

    global class GetResponseBody
    {
        public List<AccountRating> AccountRatings { get; set; }

        public GetResponseBody(IAccounts accounts)
        {
            this.AccountRatings = new List<AccountRating>();
            for (Account record : accounts.getAccounts())
            {
                this.AccountRatings.add( new AccountRating(record) );
            }
        }

        public Blob toBlob()
        {
            return Blob.valueOf(JSON.serialize(this));
        }
    }

    private class AccountRating
    {
        public Id AccountId { get; set; }
        public String Rating { get; set; }

        public AccountRating(Account record)
        {
            this.AccountId = record.Id;
            this.Rating = record.Rating;
        }
    }

    public class ErrorResponse
    {
        public String ErrorMessage { get; set; }

        public ErrorResponse(String message)
        {
            this.ErrorMessage = message;
        }

        public Blob toBlob()
        {
            return Blob.valueOf(JSON.serialize(this));
        }
    }

    public class DeveloperException extends Exception {}
}

PUT

For updating an exising entry. In this example we see that the start is the same as the Get request, but this time we use the domain setter method setRatingById to modify the values. Then we use the unitOfWork to write the changes to the database, and finally we turn a success response code.
There is no real need in this example to return a body.

@HttpPut
global static void doPut()
{
    try
    {
        // Retrieve sanitised request body
        PutRequestBody request = getPutRequestBody();

        // Validate that the user has the proper access
        fflib_SecurityUtils.checkObjectIsReadable(ACCOUNT_SOBJECT_TYPE);
        fflib_SecurityUtils.checkFieldIsReadable(ACCOUNT_SOBJECT_TYPE, Schema.Account.Rating);

        // Retrieve the records and update the fields
        IAccounts accounts = Accounts.newInstance(request.getAccountIds());
        accounts.setRatingById(request.getRatingById());

        // Send changes to the database
        fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance(
                new List<SObjectType>{ Account.SObjectType}
        );
        unitOfWork.registerDirty(accounts.getRecords());
        unitOfWork.commitWork();

        // Return a success response
        RestContext.response.statusCode = 200;
    }
    // Unauthorised to SObject
    catch (fflib_SecurityUtils.CrudException e)
    {
        RestContext.response.statusCode = 401;
        RestContext.response.responseBody =
                new ErrorResponse(CRUD_ERROR_MESSAGE).toBlob();
    }
    // Unauthorised to Field
    catch (fflib_SecurityUtils.FlsException e)
    {
        RestContext.response.statusCode = 401;
        RestContext.response.responseBody =
                new ErrorResponse(FLS_ERROR_MESSAGE).toBlob();
    }
    // JSON formatting error
    catch (System.JSONException e)
    {
        RestContext.response.statusCode = 400;
        RestContext.response.responseBody =
                new ErrorResponse(INVALID_FORMATTED_REQUEST).toBlob();
    }
    catch (Exception e)
    {
        RestContext.response.statusCode = 400;
        RestContext.response.responseBody =
                new ErrorResponse(e.getMessage()).toBlob();
    }
}

POST

To create a new entry

DELETE

The delete request is very similar to the Put request, but in this case its

@HttpDelete
global static void doDelete()
{
    try
    {
        // Retrieve sanitised request body
        DeleteRequestBody request = getDeleteRequestBody();

        // Validate that the user has the proper access
        fflib_SecurityUtils.checkObjectIsReadable(ACCOUNT_SOBJECT_TYPE);
        fflib_SecurityUtils.checkFieldIsReadable(ACCOUNT_SOBJECT_TYPE, Account.Rating);

        // Retrieve the records and update the fields
        IAccounts accounts = Accounts.newInstance(request.accountIds);
        accounts.setRating(null);

        // Send changes to the database
        fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance(
                new List<SObjectType>{ Account.SObjectType}
        );
        unitOfWork.registerDirty(accounts.getRecords());
        unitOfWork.commitWork();

        // Return a success response
        RestContext.response.statusCode = 200;
    }
    // Unauthorised to SObject
    catch (fflib_SecurityUtils.CrudException e)
    {
        RestContext.response.statusCode = 401;
        RestContext.response.responseBody =
                new ErrorResponse(CRUD_ERROR_MESSAGE).toBlob();
    }
    // Unauthorised to Field
    catch (fflib_SecurityUtils.FlsException e)
    {
        RestContext.response.statusCode = 401;
        RestContext.response.responseBody =
                new ErrorResponse(FLS_ERROR_MESSAGE).toBlob();
    }
    // JSON formatting error
    catch (System.JSONException e)
    {
        RestContext.response.statusCode = 400;
        RestContext.response.responseBody =
                new ErrorResponse(INVALID_FORMATTED_REQUEST).toBlob();
    }
    catch (Exception e)
    {
        RestContext.response.statusCode = 400;
        RestContext.response.responseBody =
                new ErrorResponse(e.getMessage()).toBlob();
    }
}

Triggering Business Logic

GET

Used for retrieving data which is the result of a complex business operation. Typically, a complex getter which retrieves data from multiple objects and/or processes.

PUT

Triggers a business process to update records

POST

Used for a business process creating data. A process that is using factory classes.

DELETE

Calls a business process to remove data.

Versioning

Always include versioning when developing webservices. Webservices can be used by multiple applications intergrating into your application, they cannot upgrade all at once when you release a new update of the REST WebService.
As you can see these examples, they contain a version number in the UrlMapping:

@RestResource(UrlMapping='/AccountRating/v1.0')
global with sharing class AccountRatingWebService { ... }
When you create a a new version of the REST WebService, you clone the latest version of the apex class and update the version number. You keep the old version for a while available so that integrating applications have time to upgrade, but make sure you plan for its removal, otherwise it will stay there indefinitely.
@RestResource(UrlMapping='/AccountRating/v2.0')
global with sharing class AccountRatingWebService2 { ... }
⚠️ **GitHub.com Fallback** ⚠️