100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Integrating Apex with LWC

Learn how Lightning Web Components call Apex with @AuraEnabled methods, wire adapters, imperative calls, caching, and error handling patterns.

Practical ApexIntermediate10 min readJul 10, 2026
Analogies

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.

javascript
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

Was this page helpful?

Topics covered

#Programming#ApexSalesforceStudyNotes#IntegratingApexWithLWC#Integrating#Apex#LWC#Exposing#StudyNotes#SkillVeris#ExamPrep