Exposing Apex Methods to Lightning Web Components
A Lightning Web Component can only call Apex methods explicitly marked @AuraEnabled, and those methods must be public static (or, for cacheable reads, public static with cacheable=true). The cacheable=true flag tells the Lightning Data Service to cache the response client-side and enables the component to use the reactive @wire adapter instead of an imperative call — but it also means the method must be a pure read (no DML, no direct record modification) and its results are eligible for reuse across components without re-hitting the server. Import the method into a component's JavaScript file using the fully qualified path @salesforce/apex/Namespace.ClassName.methodName, where Namespace is omitted for unmanaged/unpackaged orgs.
Cricket analogy: cacheable=true is like a broadcaster reusing a previously captured replay clip for a boundary instead of re-filming the shot — once captured, the same footage (data) can be reused across multiple viewers without needing a fresh call to the camera crew.
Wire Adapter vs Imperative Apex Calls
The @wire adapter provisions data reactively — the component automatically re-invokes the Apex method whenever a reactive parameter (marked with the $ prefix, like $recordId) changes, and Lightning Data Service handles caching, cache invalidation on related DML, and error state — making it the right default for read operations that should stay in sync with the record. Imperative calls (await myApexMethod({ param }) inside a JS method, typically triggered by a button click or event handler) are necessary for anything with a side effect — inserts, updates, deletes — and for cases where you need explicit control over exactly when the call fires, such as debounced search-as-you-type or a multi-step form that only submits on a final button press.
Cricket analogy: The @wire adapter automatically re-fetching on parameter change is like a live scoreboard that updates itself the instant a run is scored, without anyone manually refreshing it — reactive by design, not on-demand.
import { LightningElement, api, wire } from 'lwc';
import getOpenOpportunities from '@salesforce/apex/OpportunityController.getOpenOpportunities';
import updateOpportunityStage from '@salesforce/apex/OpportunityController.updateOpportunityStage';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class OpenOpportunityList extends LightningElement {
@api recordId; // Account Id, reactive via $recordId below
opportunities;
wiredResult;
// Reactive read: re-runs automatically whenever recordId changes
@wire(getOpenOpportunities, { accountId: '$recordId' })
wiredOpps(result) {
this.wiredResult = result; // keep reference for refreshApex()
if (result.data) {
this.opportunities = result.data;
} else if (result.error) {
this.showError(result.error);
}
}
// Imperative call: has a side effect (DML), triggered by user action
async handleStageChange(event) {
const oppId = event.target.dataset.id;
try {
await updateOpportunityStage({ oppId, newStage: 'Closed Won' });
await refreshApex(this.wiredResult); // re-sync the cached wire data
} catch (error) {
this.showError(error);
}
}
showError(error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Error updating opportunity',
message: error.body ? error.body.message : error.message,
variant: 'error'
}));
}
}Refreshing Cached Data and Handling Errors
After an imperative DML call changes data that a @wire adapter previously cached, the component's displayed data goes stale unless you explicitly call refreshApex(wiredResultReference) — Salesforce cannot automatically know your imperative call invalidated a specific cache entry, so this must be done manually and requires holding onto the full result object (not just result.data) from the wire configuration. On the error side, an @AuraEnabled method should throw an AuraHandledException (not a raw exception) for any error you want the client to see with a useful message, because unhandled exceptions are deliberately obscured by Salesforce for security reasons and surface to the client as a generic error.
Cricket analogy: Needing refreshApex() after an imperative update is like a stadium's manual scoreboard operator who has to physically re-enter the score after a boundary is signaled — the system doesn't magically know a run was scored unless someone tells it.
Always keep the full wire result object (e.g., this.wiredResult = result) in a component property, not just result.data. refreshApex() needs that full reference, including its internal cache key, to correctly invalidate and re-fetch — passing it a plain data array will not work.
Passing Data Between Apex and LWC Correctly
Apex method parameters and return types map to JavaScript types through JSON serialization: Apex Id and String become JS strings, Integer/Decimal/Double become JS numbers, Boolean maps directly, and a List<sObject> becomes a JS array of plain objects with the sObject's field API names as keys. A common pitfall is passing a JavaScript Date object directly as a parameter expecting an Apex Date or Datetime — it must be serialized to an ISO string first. For complex parameters, define an Apex inner class (or a wrapper class) with public fields matching the JSON shape you want, since @AuraEnabled methods can accept and return custom Apex types as long as their fields are also @AuraEnabled or public.
Cricket analogy: Apex-to-JS type mapping is like translating a scorecard from a paper ledger format into a digital app's data format — the numbers and names have to map cleanly (runs stay numbers, player names stay strings) or the translation breaks.
Do not throw a raw custom exception (e.g., throw new MyCustomException('bad input');) from an @AuraEnabled method and expect the client to see that message. Salesforce strips detailed error information from uncaught exceptions before sending them to the client as a security precaution. Always catch the error and re-throw as new AuraHandledException(e.getMessage()), and remember AuraHandledException itself has a quirk where you must call .setMessage() if you construct it with new AuraHandledException() before throwing, since its default constructor message isn't automatically applied the way you'd expect.
- Only public static methods marked @AuraEnabled can be called from a Lightning Web Component.
- cacheable=true enables the reactive @wire adapter and client-side caching, but restricts the method to read-only operations.
- Use @wire for reactive reads that should sync with reactive parameters; use imperative calls for anything with a DML side effect.
- After an imperative DML call, invalidate stale @wire data explicitly with refreshApex(), holding the full wire result reference.
- Apex types map to JS via JSON serialization; Dates must be passed as ISO strings and complex data needs a wrapper class.
- Uncaught Apex exceptions are stripped of detail before reaching the client — always throw AuraHandledException with an explicit message.
- Component-level field visibility is not a security control; CRUD/FLS/sharing must be enforced inside the Apex method itself.
Practice what you learned
1. What is required for an Apex method to be callable from a Lightning Web Component?
2. What restriction does the cacheable=true attribute impose on an @AuraEnabled method?
3. Why is refreshApex() needed after an imperative Apex call that performs a DML update?
4. What happens if an @AuraEnabled method throws a raw, uncaught custom exception instead of AuraHandledException?
5. How should a JavaScript Date object be passed as a parameter to an Apex method expecting a Date or Datetime?
Was this page helpful?
You May Also Like
Apex Design Patterns
Explore the Trigger Handler, Service Layer, Selector, and Unit of Work patterns that keep large Apex codebases maintainable, testable, and bulk-safe.
Security and Sharing in Apex
Learn how CRUD/FLS enforcement, sharing keywords, and the WITH SECURITY_ENFORCED / stripInaccessible tools keep Apex code from leaking data users shouldn't see.
Testing Apex Code
Learn how to write robust Apex test classes, use assertions and mocks, and produce meaningful code coverage that survives real deployments.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics