A Salesforce developer who has spent 5 years writing Apex now faces a Lightning Component requirement. The familiar option is Aura: tag-based markup, similar to JSP, comfortable. The official option is LWC: a modern JavaScript framework that looks foreign at first glance. Aura is in Maintain mode, meaning Salesforce will keep it running but not improve it. LWC is where new investment goes.
Most Apex developers approach LWC with a deeper learning curve than necessary. The framework is more straightforward than its first impression suggests. The parts an Apex dev actually needs to know are smaller than the full documentation implies.
What LWC is, briefly
Lightning Web Components are Salesforce's implementation of standard Web Components (HTML, JavaScript, CSS). Each component is three files:
- A
.htmltemplate. - A
.jsJavaScript class. - An optional
.cssstylesheet. - An optional
.js-meta.xmlconfig file.
The framework is built on web standards (custom elements, Shadow DOM, JavaScript modules). The Salesforce-specific layer is mostly: how to call Apex from the JavaScript, how to use Lightning Data Service, and the Salesforce-specific decorators.
The whole framework can be learned in a week. The first useful component can be built in a day.
A minimal LWC
A component that shows a list of Accounts:
<!-- accountList.html -->
<template>
<template if:true={accounts.data}>
<ul>
<template for:each={accounts.data} for:item="acc">
<li key={acc.Id}>{acc.Name}</li>
</template>
</ul>
</template>
<template if:true={accounts.error}>
<p>Error: {accounts.error.body.message}</p>
</template>
</template>
// accountList.js
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
@wire(getAccounts)
accounts;
}
// AccountController.cls
public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {
return [SELECT Id, Name FROM Account
WITH USER_MODE LIMIT 10];
}
}
Three files, working component. Apex method has @AuraEnabled (yes, called Aura for legacy reasons; LWC uses the same annotation). The cacheable=true flag tells LWC the result can be cached and refreshed.
Wire vs imperative Apex calls
LWC offers two patterns for calling Apex:
Wire pattern. Declarative. The Apex method runs when the component loads and reruns automatically when relevant parameters change. The result is bound to a property.
@wire(getAccounts, { searchKey: '$searchKey' })
accounts;
When searchKey changes, the wire fires again with the new value. The component re-renders automatically. Used for read operations where the result should reflect current state.
Imperative pattern. The Apex method is called from JavaScript explicitly. The component decides when to call.
import { LightningElement } from 'lwc';
import saveAccount from '@salesforce/apex/AccountController.saveAccount';
export default class AccountEditor extends LightningElement {
handleSave() {
saveAccount({ accountData: this.accountData })
.then(result => {
// Handle success.
})
.catch(error => {
// Handle failure.
});
}
}
Used for write operations and any logic that should run in response to user action.
The wire pattern handles 70% of cases. Use imperative for anything that triggers from user interaction or that needs explicit control over when it fires.
The decorators
LWC uses a few JavaScript decorators that have specific meanings:
@api: marks a property as a public API. Other components can pass values into this property. Without @api, the property is private.
@api recordId;
@track: marks a property as reactive. Changes to nested object properties (e.g., this.config.value = 'x') trigger re-renders. Salesforce auto-tracks primitives, so @track is mostly used for objects and arrays.
@track config = { value: '', count: 0 };
@wire: subscribes the property (or method) to a wire adapter. Auto-refreshes when dependencies change.
@wire(getAccountById, { id: '$recordId' })
account;
Most components use one or more of these. @api for inputs from parent or page context, @wire for data fetching, @track for complex internal state.
Lightning Data Service
For standard CRUD on records, LWC offers Lightning Data Service (LDS): a built-in cache and edit framework that bypasses Apex entirely.
import { getRecord } from 'lightning/uiRecordApi';
@wire(getRecord, { recordId: '$recordId',
fields: ['Account.Name', 'Account.Industry'] })
account;
LDS handles caching, sharing, FLS, and concurrent updates automatically. For simple read-or-write of a single record, LDS is preferable to a custom Apex method.
When to skip LDS and write Apex:
- Complex queries with custom logic.
- Aggregate operations across multiple records.
- Callouts or business rules that go beyond simple field updates.
- Cases where LDS performance is insufficient.
LDS is the right default; Apex is the escape hatch.
Events between components
Parent components pass data to children via @api properties. Children communicate back to parents via custom events:
// In the child component:
this.dispatchEvent(new CustomEvent('selected', {
detail: { accountId: this.accountId }
}));
<!-- In the parent template: -->
<c-account-list onselected={handleSelection}></c-account-list>
For components that are not in a parent-child relationship (siblings, distant cousins), use Lightning Message Service (LMS). LMS is a pub-sub framework that lets any component publish or subscribe to a message channel.
What's different from Aura
Five mental shifts for an Aura-fluent developer:
1. JavaScript modules, not Aura component bundles. Each LWC is a self-contained module. The .cmp file with attribute tags is gone; everything is JS classes and HTML templates.
2. No aura: namespace. Aura's aura:attribute, aura:handler, etc. become @api, event listeners on the class, and standard template syntax.
3. Standards-based syntax. LWC uses for:each (not aura:iteration), if:true (not aura:if), standard event syntax. Looks like vanilla web components plus a few Salesforce conventions.
4. Stricter scoping. LWC enforces Shadow DOM by default, meaning CSS does not leak between components and DOM queries are scoped to the component. Aura was more permissive; LWC is more disciplined.
5. Lifecycle hooks. LWC uses connectedCallback, renderedCallback, and disconnectedCallback (web standard). Aura's init, render, unrender are different but conceptually similar.
For most Aura components, the LWC equivalent is shorter and clearer. The translation is mostly mechanical.
Migration path from Aura
For an org with many Aura components, the recommended migration:
- New components are LWC by default. No new Aura.
- Identify high-traffic Aura components. Page Builder analytics or manual triage. These are the migration priority.
- Migrate the high-traffic components in chunks. Each migration is its own PR with regression tests.
- Leave low-traffic Aura components in place. Salesforce keeps Aura working; refactoring everything at once is rarely the right investment.
- Embed LWCs inside Aura where helpful. Aura can host LWCs, easing incremental migration of larger UIs.
A typical migration completes the high-traffic components in 3-6 months. The long tail of low-traffic Aura components may stay for years.
Common LWC mistakes for Apex devs
Five patterns Sapota has seen:
1. Building a custom component when LDS would suffice. A team writes a custom Apex method to read a single Account, when getRecord from lightning/uiRecordApi handles it natively with caching and FLS.
2. Using imperative calls for read operations. Wire is the right pattern for reads. Imperative makes the component responsible for refreshing when inputs change, which is what wire handles automatically.
3. Apex method without cacheable=true. LWC's auto-refresh and cache features need this flag on read methods. Without it, the wire pattern still works but caching is disabled.
4. Mutating wired data directly. Wired properties are read-only. Modifying this.accounts.data directly throws. Make a local copy first if mutation is needed.
5. Forgetting @api on parent-supplied properties. A component meant to receive recordId from the page context needs @api recordId;. Without the decorator, the framework does not pass the value in.
What good LWC adoption looks like
A Salesforce org with healthy LWC adoption:
- All new component work uses LWC.
- LDS used for simple CRUD; Apex reserved for complex logic.
- Wire pattern preferred for reads, imperative for writes.
- Aura components migrated by priority (high-traffic first).
- LWC tests with
@salesforce/sfdx-lwc-jestrun in CI.
Sapota's Salesforce team holds the Platform Developer I credential and runs Aura-to-LWC migrations as a defined engagement type. The transition is more mechanical than developers fear; the components emerging on the other side are easier to maintain and more performant.
Migrating Aura to LWC or starting a new Lightning component build? Sapota's Salesforce team handles LWC architecture, migration planning, and front-end testing on production engagements. Get in touch ->
See our full platform services for the stack we cover.








