All Projects → callawaycloud → apex-rest-route

callawaycloud / apex-rest-route

Licence: MIT license
A simple framework for building Restful API on Salesforce

Programming Languages

Apex
172 projects

Projects that are alternatives of or similar to apex-rest-route

apex-mocks-stress-test
Testing out FFLib versus Crud / CrudMock
Stars: ✭ 47 (-37.33%)
Mutual labels:  salesforce, apex, apex-framework
APEX-Q
A promise library for Salesforce
Stars: ✭ 30 (-60%)
Mutual labels:  salesforce, apex, apex-framework
apex-dml-mocking
DML mocking, CRUD mocking, dependency injection framework for Salesforce.com (SFDC) using Apex
Stars: ✭ 38 (-49.33%)
Mutual labels:  salesforce, apex, apex-framework
apexmock
force.com Mock data and fixtures for Apex Unit Tests
Stars: ✭ 24 (-68%)
Mutual labels:  salesforce, apex, apex-framework
sf-cross-cutting-concerns
Apex Cross cutting concerns for Salesforce
Stars: ✭ 29 (-61.33%)
Mutual labels:  salesforce, apex, apex-framework
NebulaFramework
A development framework for Salesforce's Apex language & the Force.com platform
Stars: ✭ 28 (-62.67%)
Mutual labels:  salesforce, apex, apex-framework
Vim Force.com
Vim plugin for force.com
Stars: ✭ 98 (+30.67%)
Mutual labels:  salesforce, apex
Easy Spaces Lwc
Sample application for Lightning Web Components on Salesforce Platform. Part of the sample gallery. Event management use case. Get inspired and learn best practices.
Stars: ✭ 104 (+38.67%)
Mutual labels:  salesforce, apex
Testdatafactory
The ultimate Apex Test Data Factory 🏭
Stars: ✭ 108 (+44%)
Mutual labels:  salesforce, apex
apex-rollup
Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.
Stars: ✭ 133 (+77.33%)
Mutual labels:  salesforce, apex
Sfdc Convert Attachments To Chatter Files
📎 Easily migrate your Attachments to Salesforce Files.
Stars: ✭ 72 (-4%)
Mutual labels:  salesforce, apex
Visualforce Table Grid
Flexible and highly customizable Visualforce table grid component. Salesforce.com Classic Look and Feel.
Stars: ✭ 126 (+68%)
Mutual labels:  salesforce, apex
Prettier Plugin Apex
Code formatter for the Apex Programming Language
Stars: ✭ 138 (+84%)
Mutual labels:  salesforce, apex
Awesome Low Code
Awesome Low-Code Application Platforms | 全球低代码平台开发资源大全
Stars: ✭ 90 (+20%)
Mutual labels:  salesforce, apex
Apextestkit
A way to simplify your Salesforce data creation.
Stars: ✭ 80 (+6.67%)
Mutual labels:  salesforce, apex
Query.apex
A dynamic SOQL and SOSL query builder on Salesforce.com platform
Stars: ✭ 78 (+4%)
Mutual labels:  salesforce, apex
Dreamhouse Lwc
Sample application for Lightning Web Components on Salesforce Platform. Part of the sample gallery. Real estate use case. Get inspired and learn best practices.
Stars: ✭ 136 (+81.33%)
Mutual labels:  salesforce, apex
Haoide
Stop upgrade, most of features were delivered in https://github.com/xjsender/haoide-vscode
Stars: ✭ 194 (+158.67%)
Mutual labels:  salesforce, apex
Apex Lambda
Functional programming for Salesforce Apex
Stars: ✭ 189 (+152%)
Mutual labels:  salesforce, apex
Sfdx Mass Action Scheduler
🚀 Declaratively schedule Process Builder, Flows, Quick Actions, Email Alerts, Workflow Rules, or Apex to process records from Reports, List Views, SOQL, or Apex.
Stars: ✭ 200 (+166.67%)
Mutual labels:  salesforce, apex

apex/:rest/route

A simple library that allows the creation of RESTful API's.

Features:

  • supports deeply nested RESTful resource URI
  • allows you to focus on the implementation
  • automatic responses generation
  • error responses to align with Salesforce responses
  • flexibility to override most default functionality
  • hierarchical composition encourages for code reuse and RESTful design
  • lightweight: current implementation is ~ 200LN

📦 Install

Via Unlocked Package: Install Link (update https://mydomain.salesforce.com for your target org!).

🔨 Usage

Defining Routes

Imagine you wanted to create an API to expose the follow resources Companies & CompanyLocations & CompanyEmployees

Following RESTful Design, we might have the following Resource URI definitions:

  • api/v1/companies

  • api/v1/companies/:companyId

  • api/v1/companies/:companyId/locations

  • api/v1/companies/:companyId/locations/:locationId

  • api/v1/companies/:companyId/employees

  • api/v1/companies/:companyId/employees/:employeeId

To implement this, first we will define our "Routes".

If you think of the URI as a tree, each Route should correspond to a branch:

      api/v1
        |
     companies
     |      |
locations  employees

For this example, we will just define three routes: CompanyRoute, CompanyLocationRoute & CompanyEmployeeRoute.

We could also define a top level route for api/:versionId, but for this example we'll just let that be handled by the standard @RestResource routing.

CompanyRoute will be responsible for providing a response to the following URI:

/api/v1/companies
/api/v1/companies/:companyId

CompanyLocationRoute will respond to:

/api/v1/companies/:companyId/locations
/api/v1/companies/:companyId/locations/:locationId

And CompanyEmployeeRoute will respond to:

/api/v1/companies/:companyId/employees
/api/v1/companies/:companyId/employees/:employeeId

Implementation

@RestResource(urlMapping='/v1/companies/*')
global class CompanyAPI{

    private static void handleRequest(){
      CompanyRoute router = new CompanyRoute();
      router.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();
    }
}

Things to note:

  1. This @RestResource is pretty much just a hook to call into our CompanyRoute.
  2. urlMapping='/api/v1/companies/*' defines our base route. This should always be the baseUrl for the top level router (CompanyRoute), excluding the param. IOW, the urlMapping must end with the name of the first route + /*.
public class CompanyRoute extends RestRoute {

    protected override Object doGet() {
        if (!String.isEmpty(this.resourceId)) {
           return getCompany(this.resourceId); //implementation not shown
        }
        return getCompanies();
    }

    protected override Object doPost() {
        if (!String.isEmpty(this.param)) {
          throw new RestRouteError.RestException('Create Operation does not support Company Identifier', 'NOT_SUPPORTED', 404);
        } else {
          return createCompany();
        }
    }

   //define downstream route
   protected override Map<String, RestRoute> getNextRouteMap() {
        return new Map<String, RestRoute>{
            'locations' => new CompanyLocationRoute(this.resourceId),
            'employees' => new CompanyEmployeeRoute(this.resourceId)
        };
    }
}

Things to note:

  1. Each RestRoute route is initialized with a resourceId property (if the URI contains one) and relativePaths containing the remaining URL paths from the request.

  2. The doGet & doPost corresponding to our HTTP methods for this route. Any HTTP method not implement will throw an exception. You can also override doPut, doDelete. Salesforce does not support patch at this time 🤷

  3. getNextRouteMap() will be used to determine the next RestRoute to call when the URI does not terminate with this Route. The next URI part will be matched against the Map keys. If more advanced routing is needed you can instead override the next() method and take full control.

  4. We pass this.resourceId into the next Routes so they have access to :employeeId. This composition makes it easy to provide common functionality as lower level routes much pass through their parents. For example, we could query the "Company" and pass that to the next routes instead of just this.resourceId.

public class CompanyLocationRoute extends RestRoute {
    private String companyId;

    public CompanyLocationRoute(String companyId) {
        this.companyId = companyId;
    }

    protected override Object doGet() {
        //filter down by company
        CompanyLocation[] companyLocations = getCompanyLocations(companyId);

        if (!String.isEmpty(this.resourceId)) {
            return getEntityById(this.resourceId, companyLocations);
        }
        return companyLocations;
    }
}

Things to note:

  1. We pass the companyId from the above route into the constructor
  2. This route does not implement next(). Any requests that don't end terminate with this route will result in a 404

Returning other Content-Types

By default anything your return from the doGet()|doPost()|... methods will be serialized to JSON. However, if you need to respond with another format, you can set this.response directly and return null:

protected override Object doGet() {
    this.response.responseBody = Blob.valueOf('Hello World!');
    this.response.addHeader('Content-Type', 'text/plain');
    return null;
}

Routes without resources...

While it's not exactly "Restful" you may have routes which do not always following the /:RESOURCE_URI/:RESOURCE_ID format.

For example, if you wanted to implement the following url:

/api/v1/other/foo

Note that other is not followed by a resource ID. If you want to implement foo as a RestRoute, then you need to tell other not to treat the next URL part as a :resourceId.

To do so, simply override the hasResource method:

  public class OtherRoute extends RestRoute {
      protected override boolean hasResource() {
          return false; //do parse the next url part as resourceId
      }

      protected override Map<String, RestRoute> getNextRouteMap() {
          return new Map<String, RestRoute>{ 'foo' => new FooRoute() };
      }
  }

Error Handling

You can return an Error at anytime by throwing an exception. The RestError.RestException allows you to set StatusCode and message when throwing. There are also build in Errors for common use cases (RouteNotFoundException & RouteNotFoundException).

The response body will always contain List<RestRouteError.Response> as this follows the best practices for handling REST errors.

If needed you do change this by overriding handleException(Exception err).

Expanding Results

With our above example, if we wanted to pull all information about a company we would need to make 3 request:

GET /companies/c-1

{
    "id": "c-1",
    "name": "Callaway Cloud"
}

GET /companies/123/employees

[
    {
        "id": "e-1",
        "name": "John Doe",
        "role": "Developer"
    },
    {
        "id": "e-2",
        "name": "Billy Jean",
        "role": "PM"
    }
]

GET /companies/123/locations

[
    {
        "id": "l-1",
        "name": "Jackson, Wy"
    }
]

One interesting bonus of our design is the ability for this library to "expand" results by calling expandResource(result):

public override Object doGet() {
    Company[] companies = getCompanies();
    if (!String.isEmpty(this.resourceId)) {
        Company c = (Company) getEntityById(this.resourceId, companies);
        if (this.request.params.containsKey('expand')) {
            return expandResource(c);
        }
        return c;
    }
    //... collection
}

Doing so will run all downstream routes and return a single response with the next level of data!

{
    "id": "c-1",
    "name": "Callaway Cloud",
    "employees": [
        {
            "id": "e-1",
            "name": "John Doe",
            "role": "Developer"
        },
        {
            "id": "e-2",
            "name": "Billy Jean",
            "role": "PM"
        }
    ],
    "locations": [
        {
            "id": "l-1",
            "name": "Jackson, Wy"
        }
    ]
}

This works by just running each of the child routes and merging in their data (the property is assigned based on the route; warning will overwrite any conflict).

It is even possible (although a bit more complicated) to expand on collection request.

//... doGet()
if (this.request.params.containsKey('expand')) {
    List<Map<String, Object>> expandedResponse = new List<Map<String, Object>>();
    for (Company c : companies) {
        this.resourceId = c.id;  // we must setup state for
        expandedResponse.add(expandRecord(c));
    }
    return expandedResponse;
}

While very cool, expanding collections is generally not advised due it's potential to be highly inefficient. If your downstream routes also support collection expansion, it would recursively continue through the entire tree!

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].