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.