Apex Proxy - Let's Improve How You Make Server-Side Calls!

While it’s nice that the Lightning Framework provides a way for Aura components to invoke Apex methods, the code required is a bit verbose. We can do better.

First let’s look at how Salesforce recommends you make a server-side call. The following is how you would invoke and handle one controller method from your Aura component:

getFavoriteThings: function(component, numberOfThings) {
	// Create method invocation action
	const action = component.get('c.getFavoriteThing_apex');
	
	// Set any parameters
	action.setParams({
		num: numberOfThings || 1
	});
	
	// Establish result response
	action.setCallback(this, response => {
		const state = response.getState();
		const value = response.getReturnValue();
		const error = response.getError();

		switch (state) {
			case 'SUCCESS':
				// do something with your favorite things
				console.log(value);
				break;
			case 'ERROR':
			case 'INCOMPLETE':
				// do something with the error
				console.error(error);
				break;
			case 'NEW':
			case 'RUNNING':
			default:
				break;
		}
	});
	
	// Invoke the action
	$A.enqueueAction(action);
}

Now repeat that for every different Apex method you need to invoke. Kill me now. #CodeExplosion #DontReallyKillMe

Extract. Encapsulate. Improve. Rejoice.

Luckily there’s a way to keep our sanity. Here’s a simple service component that encapsulates the required functionality, makes it argument driven, and returns a Promise.

<aura:component>
	<aura:method
		name='invoke'
		description='Invokes an @AuraEnabled method on an Apex controller.'>

		<aura:attribute
			name='caller'
			type='Object'
			description='The calling component, to which the target Apex controller has been attached.' />

		<aura:attribute
			name='method'
			type='String'
			description='The name of the method to be invoked.' />

		<aura:attribute
			name='params'
			type='Object'
			description='A map of arguments to pass with the method invocation. Each key should match the name of a parameter on the target Apex method.' />
	</aura:method>
</aura:component>
({
	invoke: function(component, event, helper) {
		const args = event.getParam('arguments');
		return helper.invoke(args.caller, args.method, args.params);
	}
})
({
	invoke: function(caller, method, params) {
		// return a promise which will either be resolved or rejected
		return new Promise($A.getCallback((resolve, reject) => {
			// Create method invocation action
			const action = caller.get('c.' + method);

			// Establish action parameters
			action.setParams(params || {});

			// Establish result response
			// Resolve on success, reject on error/incomplete, ignore otherwise
			action.setCallback(this, response => {
				const state = response.getState();
				const value = response.getReturnValue();
				const error = response.getError();
				switch (state) {
					case 'SUCCESS':
						resolve(value);
						break;
					case 'ERROR':
					case 'INCOMPLETE':
						reject(error);
						break;
					case 'NEW':
					case 'RUNNING':
					default:
						break;
				}
			});

			// Don't batch actions
			action.setBackground(true);

			// Invoke the action
			$A.enqueueAction(action);
		}));
	}
})

Using this component is as simple as including it in your other components, calling the invoke method when needed, and handling the settled promise.

<component controller='myController'>
	<!-- include the Apex Proxy in your markup -->
	<c:ApexProxy aura:id='apexProxy' />
	
	<!-- whatever else you need -->
</component>
({
	getFavoriteThing: function(component, numberOfThings) {
		// Get the Apex proxy
		const apexProxy = component.find('apexProxy');
		
		// Call the invoke method and handle the (eventual) response
		apexProxy.invoke(component, 'getFavoriteThing_apex', {num: numberOfThings})
		.then($A.getCallback(favoriteThing => {
			// Do something with your favorite thing
			console.log(favoriteThing);
		}))
		.catch($A.getCallback(error => {
			// Do something with the error
			console.error(error);
		}));
	}
})

Some notes…

  • The Apex Proxy does not need to have a controller defined for it as it automatically knows which controller to use because we pass a component instance into the invoke method.
  • Batching actions is disabled due to the fact that enqueued actions all run as one transaction and are beholden to governor limits. action.setBackground(true) ensures that the Apex invocation runs in its own transaction.

There it is - simple and easy! Later we will discuss how we can simplify our codebase even more by handling all Aura exceptions from within the Apex Proxy.

Brad Bisinger Principal Consultant