Extendable APIs
./img/joys-of-apex-thumbnail.png
API development in Apex (and pretty much every other language) is mostly bespoke, requiring the duplication of rest resources. Let's look at how we can change that using inheritance and polymorphism to power our routes. But first ...
At a certain point in everybody’s object-oriented journey, they are forced to confront a brutal truth of the world — in order to accept the power of polymorphism, sacrifices (typically) must be made. Somewhere, in the grand orchestra of closures and their concomitantly produced methods, a conductor must exist. Somewhere, things must be coupled more tightly than we would like to admit. This necessary evil wears many names; one but suits it for a short time only, and over time its etymological roots grow deeper and wider. We hear it referred to in tongues both ancient and modern, a manic susurrus with many words all converging towards one meaning:
This conductor comes in many forms, but it is this entity that gives shape — gives meaning! — to the greater system it surrounds. Like some demented Rube Goldberg machine, it says “if this, do that.” Like the punch-card machine code that once defaced (or graced, opinion-dependent) the depths of this planet, the conductor knows exactly which instruments should be playing at any given moment. It observes; it coordinates; it flourishes. Deciding how and when to create polymorphic objects is its jam.
Let’s see what good old Uncle Bob has to say on the subject:
The solution to this problem is to bury the switch statement in the basement of an ABSTRACT FACTORY, and never let anyone see it.
Uncle Bob would have our conductor live in the BASEMENT. Otherwise though, his point stands (somewhat charmingly, considering how recently Apex even got switch statements).
He also says:
Duplication may be the root of all evil in software. Many principles and practices have been created for the purpose of controlling or eliminating it.
And what does all this have to do with APIs? Is it true that we'll always need a factory method to dynamically produce polymorphic objects? Read on, dear Reader ...
1link@RestResource(urlMapping='/orders/*')
2linkglobal class OrderService {
3link @HttpPost
4link global static String post(Order order) {
5link /*do something with the order
6link possibly by instantiating another class
7link we will just skip over this not being bulkified for now ...*/
8link return 'Success';
9link }
10link}
This is fine boilerplate (probably) in the event that you only have one or two APIs communicating with external services. For distributed systems with many needs to interact with your Salesforce instance, though, you're going to quickly find yourself saying things like: "what was the syntax for creating the API again? goes and looks it up. Oh, that's right, '@RestResource', and the class needs to be global ...".
Plus, every time some other team needs to interact with some of your objects, your best case scenario is that they agree to honor contracts (in this case, another word for interfaces ...) you put forth as to the shape of the Salesforce objects / classes that you already have in place ... and you'll still need to go ahead and create the extra routes / API methods.
Can we come up with something more dynamic that enables the creation and organization of new and existing routes without all this mess? Can we do so while avoiding the typical Factory switch statement? How might we go about doing so?
Looking at that typical example, several insights may immediately jump to mind about the steps necessary to tame the beast:
Let's write a few tests for an object that will be crucial in the success of implementing a more dynamic set of rest resources within Apex. We'll need a resolver of sorts to extract what some people like to call the "needful information" from incoming rest requests (tests first!):
1link@isTest
2linkprivate class ApiRequestResolverTests {
3link @isTest
4link static void it_should_resolve_api_route() {
5link String endpointName = 'orders';
6link RestRequest req = new RestRequest();
7link req.requestURI = '/services/apexrest/api/' + endpointName + '/';
8link
9link ApiRequestResolver resolver = new ApiRequestResolver(req);
10link
11link System.assertEquals(endpointName.capitalize(), resolver.ApiName);
12link }
13link
14link @isTest
15link static void it_should_handle_non_trailing_slash() {
16link String endpointName = 'orders';
17link RestRequest req = new RestRequest();
18link req.requestURI = '/services/apexrest/api/' + endpointName;
19link
20link ApiRequestResolver resolver = new ApiRequestResolver(req);
21link
22link System.assertEquals(endpointName.capitalize(), resolver.ApiName);
23link }
24link
25link @isTest
26link static void it_should_resolve_request_body() {
27link String body = '{}';
28link RestRequest req = new RestRequest();
29link req.requestBody = Blob.valueOf(body);
30link
31link ApiRequestResolver resolver = new ApiRequestResolver(req);
32link
33link System.assertEquals(body, resolver.RequestBody);
34link }
35link
36link @isTest
37link static void it_should_resolve_request_url_param() {
38link String fakeAccountId = '0016g00000EPjVcXXX';
39link RestRequest req = new RestRequest();
40link req.requestURI = '/services/apexrest/api/account/' + fakeAccountId;
41link
42link ApiRequestResolver resolver = new ApiRequestResolver(req);
43link
44link System.assertEquals(fakeAccountId, resolver.RequestUrlBody);
45link }
46link}
And the (admittedly basic) implementation:
1linkpublic class ApiRequestResolver {
2link private final String apiBase = '/api/';
3link
4link public String ApiName { get; private set; }
5link public String RequestBody { get; private set; }
6link public String RequestUrlBody { get; private set; }
7link
8link public ApiRequestResolver(RestRequest req) {
9link this.ApiName = this.getApiName(req.requestURI);
10link this.RequestBody = req.requestBody != null ?
11link req.requestBody.toString() :
12link '';
13link //taken straight outta the docs...
14link this.RequestUrlBody = req.requestURI != null ?
15link req.requestURI.substring(req.requestURI.lastIndexOf('/') + 1)
16link : '';
17link }
18link
19link private String getApiName(String requestURI) {
20link if(requestURI == null) { return ''; }
21link Integer apiNameStart = requestURI.indexOf(apiBase) + apiBase.length();
22link Integer lastSlash = requestURI.lastIndexOf('/');
23link Integer apiNameEnd = lastSlash < apiNameStart ? requestURI.length() : lastSlash;
24link return requestURI.substring(apiNameStart, apiNameEnd).capitalize();
25link }
26link}
Our master API class is minimal. It merely defines the HTTP methods that are possible. It's route-agnostic. It creates the ApiRequestResolver
, and delegates the rest downwards:
1link@RestResource(urlMapping='/api/*')
2linkglobal class ApiService {
3link private static final ApiRequestResolver resolver =
4link new ApiRequestResolver(RestContext.request);
5link
6link @HttpDelete
7link global static Api.Response doDelete() {
8link return Api.Facade.doDelete(resolver);
9link }
10link
11link @HttpGet
12link global static Api.Response doGet() {
13link return Api.Facade.doGet(resolver);
14link }
15link
16link @HttpPatch
17link global static Api.Response patch() {
18link return Api.Facade.doPatch(resolver);
19link }
20link
21link @HttpPost
22link global static Api.Response post() {
23link return Api.Facade.doPost(resolver);
24link }
25link
26link @HttpPut
27link global static Api.Response put() {
28link return Api.Facade.doPut(resolver);
29link }
30link}
The tests are simple, and a pleasure (just one route shown for brevity's sake):
1link@isTest
2linkprivate class ApiServiceTests {
3link @isTest
4link static void it_should_return_fail_for_not_existent_route() {
5link RestContext.request = new RestRequest();
6link RestContext.request.requestURI = '/api/fake/';
7link
8link Api.Response res = ApiService.doGet();
9link
10link System.assertEquals(false, res.Success);
11link System.assertEquals(Api.BASIC_RESPONSE, res.ResponseBody);
12link }
13link
14link @isTest
15link static void it_should_return_true_for_existing_route() {
16link RestContext.request = new RestRequest();
17link RestContext.request.requestURI = '/api/test/';
18link
19link //have to insert the "namespace" for the test class
20link //otherwise it will fail to dynamically build correctly
21link Api.HANDLER_NAME = 'ApiServiceTests.' + Api.HANDLER_NAME;
22link
23link Api.Response res = ApiService.doGet();
24link
25link System.assertEquals(true, res.Success);
26link System.assertEquals(TEST_RESPONSE, res.ResponseBody);
27link }
28link
29link static String TEST_RESPONSE = 'test';
30link public class ApiHandlerTest extends Api.Handler {
31link public override Api.Response doGet(ApiRequestResolver resolver) {
32link Api.Response res = this.getResponse(TEST_RESPONSE);
33link res.Success = true;
34link return res;
35link }
36link }
37link}
At a very high level, if you were to see only these tests, you would probably understand 95% of how to interact with a given API:
Api.Handler
When I first wrote this article, I had the facade in a different, stand-alone, class ... but when I took a break from writing and thought more about it, I realized that for me, personally, encapsulating the different aspects of the API into one abstract class (a pseudo-namespace, in other words) was appealing. Your mileage may vary. I find things fit nicely into classes like this, provided they don't get too large; minimizing the surface area I need to grok when re-reading and re-remembering. Plus, the stand-alone facade class was still reaching into the Api class to make use of the constant HANDLER_NAME
— a code smell that I wasn't pleased with.
It's possible you'll have strong feelings about this pseudo-namespace and want things separated. To be clear — I think that's perfectly fine. What works for me won't always work for you. This article is meant more as an exercise in the creation of dynamic APIs rather than an interjection on the best file structure:
1linkglobal abstract class Api {
2link public static String HANDLER_NAME = 'ApiHandler';
3link public static final String BASIC_RESPONSE = 'HTTP method not yet implemented';
4link
5link global class Response {
6link global Response(String body) {
7link this.Success = false;
8link this.ResponseBody = body;
9link }
10link
11link public Boolean Success { get; set; }
12link public String ResponseBody { get; private set; }
13link }
14link
15link public static Facade Facade {
16link get {
17link if(Facade == null) {
18link Facade = new Facade();
19link }
20link return Facade;
21link }
22link private set;
23link }
24link
25link public class Facade {
26link private Facade() { }
27link
28link public Response doDelete(ApiRequestResolver resolver) {
29link return this.getHandler(resolver).doDelete(resolver);
30link }
31link
32link public Response doGet(ApiRequestResolver resolver) {
33link return this.getHandler(resolver).doGet(resolver);
34link }
35link
36link public Response doPatch(ApiRequestResolver resolver) {
37link return this.getHandler(resolver).doPatch(resolver);
38link }
39link
40link public Response doPost(ApiRequestResolver resolver) {
41link return this.getHandler(resolver).doPost(resolver);
42link }
43link
44link public Response doPut(ApiRequestResolver resolver) {
45link return this.getHandler(resolver).doPut(resolver);
46link }
47link
48link public Handler getHandler(ApiRequestResolver resolver) {
49link Type handlerType = Type.forName(Api.HANDLER_NAME + resolver.ApiName);
50link return handlerType != null ? (Api.Handler)handlerType.newInstance() : new Api.Handler();
51link }
52link }
53link
54link public virtual class Handler {
55link //enforce zero argument constructor
56link public Handler() { }
57link
58link public virtual Response doDelete(RequestResolver resolver) {
59link return getResponse(BASIC_RESPONSE);
60link }
61link public virtual Response doGet(RequestResolver resolver) {
62link return getResponse(BASIC_RESPONSE);
63link }
64link public virtual Response doPatch(RequestResolver resolver) {
65link return getResponse(BASIC_RESPONSE);
66link }
67link public virtual Response doPost(RequestResolver resolver) {
68link return getResponse(BASIC_RESPONSE);
69link }
70link public virtual Response doPut(RequestResolver resolver) {
71link return getResponse(BASIC_RESPONSE);
72link }
73link
74link protected Response getResponse(String responseBody) {
75link return new Response(responseBody);
76link }
77link }
78link}
So, in the end, our conductor has been delegated to Type.forName(String yourTypeName)
— a coupling that I ordinarily tend to > > shy away from; within the system, it's easy to pass around Type references (for those following along, you may recall that that > > is precisely how the Callback interface works), but it's not as though we > > have that luxury when interacting with cross-system consumers. Furthermore, it makes sense that if a tight coupling should > > > > exist, that it be between the name of the API and the underlying objects.
Now, I'm not saying that I know the underlying specifics of how "Type.forName" is implemented ... but if I had to guess Salesforce just does a lookup to the ApexClass table based on the name you provide (and the namespace, if you're in the business of creating managed packages). Not quite an if/else or switch statement, but since we're delegating ... who knows!
I'll end this section by saying that I think there's plenty of room for improvement in what I'm showing off. In an actual implementation of this, for example, I would likely:
One thing I considered, but wouldn't actually do? Wrapping the virtual methods within the Handler
in try/catch. If your operation has some chance of failing, it should be within the override that you try/catch.
Implementing new APIs off of the base /api/
route is now as simple as adding classes that extend Api.Handler
with names that start with "ApiHandler" (of course you could tweak "ApiHandler" if that naming convention doesn't appeal to you) — I'll show an example API designed to fetch accounts. It should be noted that this GET request can be formulated either with:
https://instance.salesforce.com/services/apexrest/api/account/ACCOUNT_ID_HERE
https://instance.salesforce.com/services/apexrest/api/account/
with the Id in the request body (though I haven't shown this approach)1linkpublic class ApiHandlerAccount extends Api.Handler {
2link public final static String NOT_FOUND = 'Account Not Found';
3link
4link private final IRepository accountRepo;
5link
6link public ApiHandlerAccount() {
7link super();
8link //install your dependencies via the Factory
9link this.accountRepo = Factory.getFactory().RepoFactory.getAccountRepo();
10link }
11link
12link public override Api.Response doGet(ApiRequestResolver resolver) {
13link Id accountId = Id.valueOf(resolver.RequestUrlBody);
14link List<Account> accounts = this.accountRepo.get(
15link new Query(
16link Account.Id,
17link Query.Operator.EQUALS,
18link accountId
19link )
20link );
21link Account acc = accounts.size() > 0 ?
22link accounts[0] :
23link new Account(Name = NOT_FOUND);
24link
25link Api.Response res = this.getResponse(Json.serialize(acc));
26link res.Success = accounts.size() > 0;
27link return res;
28link }
29link}
You probably wouldn't be serializing the whole Account to return to your calling service — this is just an example!
If looking at that Factory statement has you going huh!?! then I'll kindly refer you to the Factory & Dependency Injection post. The whole downside of Type.forName
— that your object is required to have a zero-argument constructor — can be recovered from if you have the ability to quickly swap out the objects of your choice when testing, plus you can actually assert that you're querying for the correct things, as I'll show with the test:
1link@isTest
2linkprivate class ApiHandlerAccountTests {
3link @isTest
4link static void it_should_return_stringified_account_to_api() {
5link /*most of my objects have a one-argument
6link constructor that takes in the factory
7link and they use the following syntax:
8link Factory.withMocks.getClassHere.methodUnderTest
9link for zero-argument constructors, we have to explicitly call
10link useMocks prior to testing */
11link Factory.useMocks();
12link //arrange
13link Id accountId = TestingUtils.generateId(Account.SObjectType);
14link Account fakeAccount = new Account(Id = accountId, Name = 'ApiHandlerTest');
15link RepoFactoryMock.QueryResults.add(fakeAccount);
16link RestContext.request = getAccountRequest(accountId);
17link
18link //act
19link Api.Response res = ApiService.doGet();
20link Account deserializedAccount = (Account)Json.deserialize(
21link res.ResponseBody,
22link Schema.Account.class
23link );
24link
25link //assert
26link System.assertEquals(true, res.Success);
27link System.assertEquals(accountId, deserializedAccount.Id);
28link System.assertEquals(fakeAccount.Name, deserializedAccount.Name);
29link
30link Query performedQuery = RepoFactoryMock.QueriesMade[0];
31link System.assertEquals(Account.Id, performedQuery.field);
32link System.assertEquals(Query.Operator.EQUALS, performedQuery.operator);
33link System.assertEquals(accountId, performedQuery.predicates[0]);
34link }
35link
36link @isTest
37link static void it_should_return_account_not_found_for_no_results() {
38link //no need to mock the result
39link //we can go straight to the db here
40link //even though there's no result
41link // it's still 8x slower than mocking!!
42link RestContext.request = getAccountRequest(
43link TestingUtils.generateId(Account.SObjectType)
44link );
45link
46link Api.Response res = ApiService.doGet();
47link Account deserializedAccount = (Account)Json.deserialize(
48link res.ResponseBody,
49link Schema.Account.class
50link );
51link
52link System.assertEquals(false, res.Success);
53link System.assertEquals(ApiHandlerAccount.NOT_FOUND, deserializedAccount.Name);
54link }
55link
56link static RestRequest getAccountRequest(Id accountId) {
57link RestRequest req = new RestRequest();
58link req.requestURI = '/api/account/' + accountId;
59link return req;
60link }
61link}
And there you have it. Our first test runs in two-hundredths of a second on average, making the one-tenth of a second for the second test (the one that actually performs a SOQL query) seem positively pokey by comparison. Our first test is capable of verifying not only that the expected Account is returned to us, but also that the query that returns the Account is in fact exactly what we expect. That's huge.
Thanks for reading this Joys Of Apex post! I sincerely hope you learned something and enjoyed the process. The full code from this example is available on my Github if you'd like to browse through the classes in a bit more detail. It's my hope that the code will prove thought-provoking when you consider your existing APIs, and that you would consider using it on a greenfield project; for the truly brave, there's little downside to gradually migrating your existing APIs to be housed under a single route.
Of course, as with any concept, what I've talked about here doesn't cover the full breadth of what you'll need to consider when using this pattern with SFDC. Just off the top of my head:
RestContext.request
to be sufficient for most people's needs. The ApiRequestResolver
could also be souped-up to hold these values if you, like me, dislike people accessing static globals all over the place.The original version of Extendable APIs can be read on my blog.
Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Formula Date Issues Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Replacing DLRS With Custom Rollup Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests