Apex for Lightning Web Components

Rules: methods must be static and public or global; cacheable=true methods must not perform DML (read-only, results cached client-side by Lightning Data Service); return types must be serializable (sObjects, primitives, collections, classes with @AuraEnable…

30.1 Exposing Apex to LWC

Rules: methods must be static and public or global; cacheable=true methods must not perform DML (read-only, results cached client-side by Lightning Data Service); return types must be serializable (sObjects, primitives, collections, classes with @AuraEnabled properties).

public with sharing class AccountController {

    @AuraEnabled(cacheable=true)
    public static List<Account> searchAccounts(String searchKey) {
        String key = '%' + (searchKey == null ? '' : searchKey) + '%';
        return [
            SELECT Id, Name, Industry, AnnualRevenue
            FROM Account
            WHERE Name LIKE :key
            WITH USER_MODE
            ORDER BY Name
            LIMIT 50
        ];
    }

    @AuraEnabled
    public static Id createAccount(String name, String industry) {
        try {
            Account a = new Account(Name = name, Industry = industry);
            insert as user a;
            return a.Id;
        } catch (DmlException e) {
            throw new AuraHandledException(e.getDmlMessage(0));    // friendly message to the client
        }
    }
}

30.2 Wire vs imperative calls

| | @wire | Imperative | |-|---------|-----------| | Requires cacheable=true | Yes | No (optional) | | When it runs | Automatically, re-runs when reactive params ($prop) change | When your code calls it | | DML allowed | No | Yes (non-cacheable) | | Refresh | refreshApex(wiredResult) | Call again | | Typical use | Displaying data | Button clicks, saves, conditional loading |

The matching template, accountSearch.html:

// accountSearch.js
import { LightningElement, wire } from 'lwc';
import searchAccounts from '@salesforce/apex/AccountController.searchAccounts';
import createAccount from '@salesforce/apex/AccountController.createAccount';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class AccountSearch extends LightningElement {
    searchKey = '';
    wiredResult;
    accounts;
    error;

    // wire: reactive — re-queries when searchKey changes
    @wire(searchAccounts, { searchKey: '$searchKey' })
    wiredAccounts(result) {
        this.wiredResult = result;
        if (result.data) { this.accounts = result.data; this.error = undefined; }
        else if (result.error) { this.error = result.error; this.accounts = undefined; }
    }

    handleKeyChange(event) {
        this.searchKey = event.target.value;
    }

    // imperative: on demand, may perform DML
    async handleCreate() {
        try {
            const id = await createAccount({ name: 'New Co', industry: 'Retail' });
            this.dispatchEvent(new ShowToastEvent({ title: 'Created', message: id, variant: 'success' }));
            await refr
…
<template>
    <lightning-card title="Account Search" icon-name="standard:account">
        <div class="slds-p-around_medium">
            <lightning-input label="Search" value={searchKey} onchange={handleKeyChange}></lightning-input>
            <template lwc:if={accounts}>
                <template for:each={accounts} for:item="acc">
                    <p key={acc.Id}>{acc.Name} — {acc.Industry}</p>
                </template>
            </template>
            <template lwc:if={error}><p class="slds-text-color_error">Error loading accounts</p></template>
            <lightning-button label="Create" onclick={handleCreate}></lightning-button>
        </div>
    </lightning-card>
</template>

30.3 Passing complex data

public with sharing class OrderController {
    public class OrderInput {
        @AuraEnabled public String accountId { get; set; }
        @AuraEnabled public List<String> skus { get; set; }
        @AuraEnabled public Decimal discount { get; set; }
    }
    public class OrderSummary {
        @AuraEnabled public Id orderId;
        @AuraEnabled public Decimal total;
    }

    @AuraEnabled
    public static OrderSummary placeOrder(OrderInput input) {
        OrderSummary s = new OrderSummary();
        // ... business logic delegated to a service class ...
        return s;
    }
}
const summary = await placeOrder({ input: { accountId: this.recordId, skus: ['A1', 'B2'], discount: 5 } });

30.4 Tips

Prefer Lightning Data Service (lightning-record-form, getRecord, updateRecord, GraphQL wire adapter) for single-record CRUD — no Apex needed, and FLS/sharing are enforced.

Keep controllers thin: validate input, call services, map to DTOs.

Always throw AuraHandledException with safe messages; other exceptions show generic errors to users.

@AuraEnabled(cacheable=true scope='global') allows CDN-style caching for Experience Cloud sites (only for data that is the same for all users).

Use continuation=true on @AuraEnabled methods that return a Continuation for long-running callouts.