Skip to content

JJLongoria/sf-rest-service-framework

Repository files navigation

The REST Service Framework are designed to create a complete API REST on Salesforce easy, with only one entry point (one @RestResource class) to manage an entire standard API REST easy to focus your efforts on routes definition and implementation.

This framework simplify manage and handle any API REST because use one (or a few) api entry points and not need to remember all entry point classes, only define your routes, the routing and implement your routes, let the framework make the rest.

You only need to extends the class RestServiceRoute on any Rest API Route to create your API.

For example, we need to define the next routes on our API to work with Accounts, Opportunities and Contacts:

  • api/v1.0/accounts/
  • api/v1.0/accounts/:accountId

  • api/v1.0/accounts/:accountId/contacts
  • api/v1.0/accounts/:accountId/contacts/:contactId
  • api/v1.0/contacts
  • api/v1.0/contacts/:contactId

  • api/v1.0/accounts/:accountId/opportunities
  • api/v1.0/accounts/:accountId/opportunities/:oppId
  • api/v1.0/opportunities
  • api/v1.0/opportunities/:oppId

The Tree routes are the next:

            api/v1.0
        _______|_______
        |      |      |
        |   Accounts  |
        |______|______|
        |             |
    Contacts     Opportunities           

According the tree routes, we have an entry point api/v1.0 and, at least, three main routes (classes), Accounts, Contacts and Opportunidades

We can transform the routes on classes like this:

  • api/v1.0 => APIEntryPointRoute
  • Accounts => AccountsRoute
  • Contacts => ContactsRoute
  • Opportunities => OpportunitiesRoute

With this design, all main routes will be defined in APIEntryPointRoute, that is, accounts, contacts and opportunities routes and their controller classes (AccountsRoute, ContactsRoute and OpportunitiesRoute)

De esta forma la definición de las rutas principales de la API estarán en APIEntryPointRoute es decir, las rutas, accounts, contacts y opportunities estarán aqui definidas, así como su clase controladora respectivamente (AccountsRoute, ContactsRoute y OpportunitiesRoute)

Therefore, the AccountsRoute class will manage the next routes

  • api/v1.0/accounts/
  • api/v1.0/accounts/:accountId

and **ContactsRoute**class will handle this routes

  • api/v1.0/accounts/:accountId/contacts
  • api/v1.0/accounts/:accountId/contacts/:contactId
  • api/v1.0/contacts
  • api/v1.0/contacts/:contactId

and last, the OpportunitiesRoute class will be respond to all this routes

  • api/v1.0/accounts/:accountId/opportunities
  • api/v1.0/accounts/:accountId/opportunities/:oppId
  • api/v1.0/opportunities
  • api/v1.0/opportunities/:oppId

With this framework, we can define only one entry point to all our REST APIs on our Salesforce project, that is, onle one class with @RestResource tag, because the framework will handle and routing all requests.

@RestResource(urlMapping='/api/*')
global class APIRestEntryPoint{

    private static void handleRequest(){
      APIEntryPointRoute route = new APIEntryPointRoute();
      route.execute();
    }

    @HttpGet
    global static void handleGet() {
        handleRequest();
    }

    @HttpPost
    global static void handlePost() {
        handleRequest();
    }

    @HttpPut
    global static void handlePut() {
        handleRequest();
    }

    @HttpDelete
    global static void handleDelete() {
        handleRequest();
    }
}

No need implement any more on the entry point class, the entire implementation will be handle by APIEntryPointRoute as main router class.


To implement the routes, we must create the classes APIEntryPointRoute, AccountsRoute, ContactsRoute and OpportunitiesRoute to handle REST Requests.

public class APIEntryRoute extends RestServiceRoute {

    // This class only need to implement (excep to handle other requests) the setupRoutes() method (inherited) to setup the API Routing

    public override void setupRoutes() {
        // use method addRoute() (inherited) to add any route to the API
        addRoute('accounts', new AccountsRoute());
        addRoute('contacts', new ContactsRoute());
        addRoute('opportunities', new OpportunitiesRoute());
    }
}

The Account route implementation example are:

public class AccountsRoute extends RestServiceRoute {

    // Setup child routes
    public override void setupRoutes() {
        // use method addRoute() (inherited) to add any child route to account endpoint
        addRoute('contacts', new ContactsRoute(getResourceId()));
        addRoute('opportunities', new OpportunitiesRoute(getResourceId()));
    }

    public override Object doGet() {
        if (!String.isEmpty(getResourceId())) {
            // Handle request when have resourceId on URL
            // Endpoint: api/v1.0/accounts/:accountId
            Account acc = new Account();
            // Implementation not shown
            return acc;
        } else if (containsQueryParameter('accountId')){
            String recordId = getQueryParameter('accountId');
            // Handle request when not has resourceId on URL but has resourceId on URL query parameter
            // Endpoint: api/v1.0/accounts?accountId=XXXXXXX
            Account acc = new Account();
            // Implementation not shown
            return acc;
        } else {
            // Handle request when not has resource Id on URL
            // Endpoint: api/v1.0/accounts
            List<Account> acc = new List<Account>();
            // Implementation not shown
            return acc;
        }
    }

    // Include methods doPost(), doPut() or doDelete() to handle other requests
}

To implement the example contacts route, you can do the next:

public class ContactsRoute extends RestServiceRoute {

    private String accountId;

    public ContactsRoute(){

    }

    public ContactsRoute(String accountId){
        this.accountId = accountId;
    }

    public override Object doGet() {
        // To get the account Id if has on URL Query parameters
        // Endpoint: api/v1.0/contacts?accountId=XXXXXX
        if (this.accountId == null && containsQueryParameter('accountId')) {
            this.accountId = getQueryParameter('accountId');
        }

        if (!String.isEmpty(getResourceId())) {
            // Handle request when have resourceId on URL
            // Endpoint: api/v1.0/accounts/:accountId/contacts/:contactId
            // or Endpoint: api/v1.0/contacts/:contactId
            Contact contact;
            if (!String.isEmpty(this.accountId)){
                // Handle request when has accountId
                // Endpoint: api/v1.0/accounts/:accountId/contacts/:contactId
                // or Endpoint: api/v1.0/contacts/:contactId/?accountId=XXXXXX
                // Implementation not shown
                return contact;
            } else{
                // Handle request when not has accountId
                // Endpoint: api/v1.0/contacts/:contactId/
                // Implementation not shown
                return contact;
            }
            return contact;
        } else if (!String.isEmpty(this.accountId)) {
            // Handle request when not has resourceId but with acoountId like query parameter
            // Endpoint: api/v1.0/contacts?accountId=XXXXXX
            List<Contact> contacts = List<Contact>();
            // Implementation not shown
            return contacts;
        } else {
            // Handle request when not has resourceId or accountId
            // Endpoint: api/v1.0/contacts
            List<Contact> contacts = List<Contact>();
            // Implementation not shown
            return contacts;
        }
    }

    // Include methods doPost(), doPut() or doDelete() to handle other requests
}

The Opportunities route implementation will be simillar like the other routes.


This Framework are designed to handle errors automatically when you use any class that extends from RestService.RestException class, that is, you can crete your custom exceptions to handling errors.

By default, the framework work with the estandard JSONAPI 1.0 to return errors, but your can return any other object as response when you want. Into RestServiceError has all classes to handle errors with JSONAPI 1.0

To create custom exceptions:

public class MyCustomException extends RestServiceException {
    // The object errorResponse will be serialized and included into the response body automatically when you throw any exception.
    public MyCustomException(String message, Integer httpStatus, Object errorResponse) {
        super(message, httpStatus, errorResponse);
    }
}

You can throw or exception on any moment to handle errors and will be serialized and included into the response body automatically, including the httpStatus value into the response status code.

If you need to handle customized errors, can override the handleException() on any route to make your custom code error handling.

The actual method do the next:

protected virtual void handleException(Exception ex) {
    if (ex instanceof RestService.RestServiceException) {
        RestService.RestServiceException restErr = (RestService.RestServiceException)ex;
        response.statusCode = restErr.status;
        response.responseBody = (restErr.errorResponse != null) ? Blob.valueOf(JSON.serialize(restErr.errorResponse)) : response.responseBody;
    } else {
        throw ex;
    }
}

And we can override and make anything with it

public class ContactsRoute extends RestServiceRoute {

    public override Object doGet() {
        // Code
    }

    public override Object doPost() {
        // Mode Code
    }

    ///.... other methods

    // override method to handle errors customized
    protected override void handleException(Exception ex) {
        if (ex instanceof RestService.RestServiceException) {
            // Handle framework exceptions
        } else {
            // Handle other any exception
        }
    }

}

Only on special cases you will need to override the handleException() method



The Rest Service Framework return by default JSON data (Content-Type=application/json), because serialize automatically the returned response object by the route methods to include on response body. If you need to return any other data type, can use the methods setContentType('value') and setResponseBody() (both inherited) to set the content type and body to your response. (You can use this.response like route property to modify anything of the response)

Example:

public class CustomContentExampleRoute extends RestServiceRoute {
    protected override Object doGet() {
        setContentType('text/plain');
        setResponseBody('This response is not a JSON Response');
        // Return null to not include anything on body serialized as JSON (default behaviour)
        return null;
    }
}

If not return null, the response body setted with setResponseBody() method will be override.



Sometimes, you need to implement the not standard REST routes like /:RESOURCE_URI/:RESOURCE_ID, for example, the next route:

  • /api/v1/routeWithoutParam/otherRoute

The routeWithoutParam route has not resourceId, in this case the next route are otraRuta. To implement this cases, you can do:

public class RouteWithoutResourceExampleRoute extends RestServiceRoute {

    public RouteWithoutResourceExampleRoute(){
        // On the constructor we call withoutResourceId() method to indicate to the framework that this route has not parameters
        withoutResourceId();
    }

    // setup child routes
    public override void setupRoutes() {
        // use addRoute() method (inherited) to add any route
        addRoute('otherRoute', new OtherRoute());
    }

    protected override Object doGet() {
       // Code...
    }
}

An interesting framework function is the ability to expand the response object, that is, get all child routes data into one single response object. For example we can call the endpoint api/v1.0/accounts/:accountId and get the entire data from:

  • api/v1.0/accounts/:accountId/contacts
  • api/v1.0/accounts/:accountId/opportunities

To choose the expand response, call endpoints like this: api/v1.0/accounts/:accountId?expand=true

public class AccountRoute extends RestServiceRoute {

    // Setu child routes
    public override void setupRoutes() {
        // use addRoute() method (inherited) to add any route
        addRoute('contacts', new ContactsRoute(getResourceId()));
        addRoute('opportunities', new OpportunitiesRoute(getResourceId()));
    }

    public override Object doGet() {
        if (!String.isEmpty(getResourceId())) 
            Account acc = // Get account
            // Use expandResponse() method to check if need to expand the response
            if (expandResponse()) {
                return expand(acc);
            }
            return acc;
        }
        //... collection
    }
}

The expanded response will be like this:

{
    "Id": "XXXXXXXXXXXX",
    "Name": "Account Example Name",
    "OtherAccountField": "Value",
    "contacts": [
        {
            "Id": "YYYYYYYYYYYYYY",
            "FirtName": "Contact 1 Firt Name",
            "LastName": "Contact 1 Last Name"
        },
        {
            "Id": "YYYYYYYYYYYYYY",
            "FirtName": "Contact 2 Firt Name",
            "LastName": "Contact 2 Last Name"
        }
    ],
    "opportunities": [
        {
            "Id": "ZZZZZZZZZZZZZ",
            "Name": "Opportunity Name",
            "StageName": "Won"
        }
    ]
}

The RestServiceRoute class include to many inherited methods to use on any implement route to make easy implement APIs:

  • withoutResourceId(): To indicate that the route has not resource Id
  • getResourceId(): To get the resource id from URL
  • loadResource(): Method with several overloads to load a single resource (record) from database
  • loadResources(): Method with several overloads to load a resources list (records) from database
  • loadRelatedResource(): Method with several overloads to load a single related resource (record) from database
  • loadRelatedResources(): Method with several overloads to load related resources (records) from database
  • expandResponse(): Method to check if must return an expanded response
  • expand(): Method to expand the response with all endpoint data (with child endpoints data)
  • addRoute(): Method to add child routes to any route

The RestServiceRoute class also inherit from RestService class and contains other interesting methods to use on childs:

  • request: Property to get the Rest Request object
  • response: Property to get the Rest Response object
  • getQueryParameters(): Method to get the request query parameters map
  • containsQueryParameter(): Method to check if exists the selected request query parameter
  • getQueryParameter(): Method to get the selected request query paramter value
  • getRequestHeaders(): Method to get the request headers map
  • containsRequestHeader(): Method check if the request contains the selected header
  • getRequestHeader(): Method to get the request header selected value
  • addResponseHeader(): Method to add headers to response
  • setContentType(): To set the response content type (to use different from application/json)
  • setResponseBody(): To set the response body content (to use different from JSON responses)

Contributions

Releases

No releases published

Packages

No packages published

Languages