Lightning Web Components (LWC) Beginner Guide: Build Your First Component in 30 Minutes
By Firus Hanov ยท ยท 11 min read
Complete beginner guide to Lightning Web Components. Learn LWC file structure, data binding, @wire, imperative Apex, component communication, and build a real Contact Card component.

Lightning Web Components (LWC) is Salesforce's modern UI framework, and if you've been building in Salesforce for more than a year, you need to know it. Whether you're coming from Aura, Visualforce, or starting fresh โ LWC is where Salesforce UI development lives today.
The good news: LWC is built on modern web standards. If you know JavaScript, HTML, and CSS, you already know a lot of what makes LWC work. The Salesforce-specific parts are actually pretty small.
By the end of this guide, you'll understand how LWC components are structured, how data flows between components, how to call Apex from LWC, and you'll have built your first real component. ๐ฅ
What is Lightning Web Components?
LWC is a component-based framework that Salesforce introduced in 2019 to replace the older Aura (Lightning) framework. It's built on web standards โ Custom Elements, Shadow DOM, and ES Modules โ rather than proprietary abstractions.
This matters because:
- Better performance: Web standards are optimized by browsers natively
- Smaller learning curve: Standard JavaScript patterns, no framework-specific oddities
- Easier testing: Standard JavaScript testing tools work out of the box
- Future-proof: As web standards evolve, LWC evolves with them
LWC is used for record detail pages, app pages, utility bars, custom tabs, communities, email templates โ essentially anywhere you need a custom UI in Salesforce.
LWC File Structure
Every LWC component lives in its own folder. The folder name becomes the component's API name.
myComponent/
โโโ myComponent.html โ Template (required)
โโโ myComponent.js โ Controller logic (required)
โโโ myComponent.css โ Styles (optional)
โโโ myComponent.js-meta.xml โ Metadata (required for deployment)
โโโ __tests__/
โโโ myComponent.test.js โ Jest tests (recommended)
Let's look at each file.
The HTML Template
<!-- myComponent.html -->
<template>
<lightning-card title="My First Component" icon-name="standard:account">
<div class="slds-m-around_medium">
<p>Hello, {greeting}!</p>
<lightning-button
label="Say Hello"
onclick={handleClick}>
</lightning-button>
</div>
</lightning-card>
</template>
Key rules for LWC templates:
- Must have a single
<template>root element - Data binding uses
{propertyName}โ single curly braces, NOT double like Angular/Vue - Event handlers reference a method with
{methodName}syntax - Conditional rendering uses
lwc:if,lwc:elseif,lwc:else - List iteration uses
for:eachandfor:item
The JavaScript Controller
// myComponent.js
import { LightningElement, track } from 'lwc';
export default class MyComponent extends LightningElement {
greeting = 'World';
handleClick() {
this.greeting = 'Salesforce Developer';
}
}
Important concepts:
- Your class must extend
LightningElement - Properties bound in the template are reactive by default in modern LWC
@apimakes a property public (settable by parent components)@trackis no longer needed for most reactive properties (it's the default now)
The CSS File
/* myComponent.css */
.greeting-text {
font-weight: bold;
color: #1b5e20;
}
LWC CSS is automatically scoped to the component via Shadow DOM. Your styles won't accidentally affect other components, and external styles won't bleed in.
The Metadata File
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
isExposed: true makes the component available in Lightning App Builder. The targets define where it can be placed.
Your First Real Component: Contact Card
Let's build something useful โ a component that displays a contact's details and lets you update their title.
The Apex Class
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static Contact getContact(Id contactId) {
return [
SELECT Id, FirstName, LastName, Title, Email, Phone, AccountId,
Account.Name
FROM Contact
WHERE Id = :contactId
WITH SECURITY_ENFORCED
];
}
@AuraEnabled
public static void updateContactTitle(Id contactId, String newTitle) {
Contact c = new Contact(Id = contactId, Title = newTitle);
update c;
}
}
Key points:
@AuraEnabledmakes the method callable from LWCcacheable=trueenables client-side caching (use for read-only queries)with sharingenforces the user's sharing rulesWITH SECURITY_ENFORCEDenforces field-level security at the query level
The Component HTML
<template>
<lightning-card title="Contact Details" icon-name="standard:contact">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
</template>
<template lwc:elseif={error}>
<p class="slds-text-color_error">Error loading contact: {error}</p>
</template>
<template lwc:elseif={contact}>
<div class="slds-m-around_medium">
<p class="slds-text-heading_medium">
{contact.FirstName} {contact.LastName}
</p>
<p>{contact.Email}</p>
<p>{contact.Phone}</p>
<div class="slds-m-top_medium">
<lightning-input label="Update Title" value={newTitle} onchange={handleTitleChange}></lightning-input>
<lightning-button label="Save Title" variant="brand" onclick={handleSave} class="slds-m-top_small"></lightning-button>
</div>
</div>
</template>
</lightning-card>
</template>
The Component JavaScript
import { LightningElement, api, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getContact from '@salesforce/apex/ContactController.getContact';
import updateContactTitle from '@salesforce/apex/ContactController.updateContactTitle';
export default class ContactCard extends LightningElement {
@api recordId;
contact;
error;
newTitle = '';
isLoading = false;
wiredContactResult;
@wire(getContact, { contactId: '$recordId' })
wiredContact(result) {
this.wiredContactResult = result;
if (result.data) {
this.contact = result.data;
this.newTitle = result.data.Title || '';
this.error = undefined;
} else if (result.error) {
this.error = result.error.body?.message || 'Unknown error';
this.contact = undefined;
}
}
handleTitleChange(event) {
this.newTitle = event.target.value;
}
async handleSave() {
this.isLoading = true;
try {
await updateContactTitle({ contactId: this.recordId, newTitle: this.newTitle });
this.dispatchEvent(new ShowToastEvent({ title: 'Success', message: 'Contact title updated!', variant: 'success' }));
await refreshApex(this.wiredContactResult);
} catch (error) {
this.dispatchEvent(new ShowToastEvent({ title: 'Error', message: error.body?.message || 'Update failed', variant: 'error' }));
} finally {
this.isLoading = false;
}
}
}
This component demonstrates:
@api recordIdโ receives the current record's ID automatically@wireโ reactive data fetching that auto-updatesasync/awaitโ clean async pattern for imperative Apex callsShowToastEventโ standard success/error notificationsrefreshApexโ re-fetches wire data after a DML operation
Understanding @wire vs Imperative Apex
@wire โ Declarative, Reactive
@wire(getContact, { contactId: '$recordId' })
wiredContact({ data, error }) {
// Auto-runs when contactId changes
// Auto-caches results
}
Use @wire when:
- You want data loaded automatically on component render
- The data is read-only
- You want automatic reactivity to property changes
Imperative โ Manual, Controlled
async handleButtonClick() {
try {
const result = await myApexMethod({ param: this.someValue });
this.data = result;
} catch (error) {
// Handle error
}
}
Use imperative when:
- You need the call triggered by a user action
- The Apex method has side effects (DML)
- You need precise control over when the call happens
Component Communication
Parent to Child: Properties
<c-child-component greeting="Hello from Parent" record-id={currentId}></c-child-component>
import { LightningElement, api } from 'lwc';
export default class ChildComponent extends LightningElement {
@api greeting;
@api recordId;
}
Child to Parent: Custom Events
// Child fires event
handleButtonClick() {
this.dispatchEvent(new CustomEvent('itemselected', {
detail: { selectedId: this.itemId }
}));
}
// Parent handles event
handleItemSelected(event) {
const selectedId = event.detail.selectedId;
}
Conditional Rendering and Lists
<template>
<template lwc:if={showDetails}>
<p>Details are visible</p>
</template>
<template lwc:else>
<p>Details are hidden</p>
</template>
<template for:each={items} for:item="item">
<div key={item.id}>{item.name}</div>
</template>
</template>
Note: The key attribute is required on the immediate child of for:each.
Quick Reference: Essential LWC Imports
import { LightningElement, api, wire, track } from 'lwc';
import myMethod from '@salesforce/apex/MyClass.myMethod';
import userId from '@salesforce/user/Id';
import myLabel from '@salesforce/label/c.My_Custom_Label';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { NavigationMixin } from 'lightning/navigation';
import NAME_FIELD from '@salesforce/schema/Contact.Name';
Next Steps
You now understand the fundamentals of LWC: file structure, properties, wire service, imperative Apex, component communication, and conditional rendering. That's enough to build real, production-ready components.
To level up from here:
- Lightning Data Service (LDS): Use
lightning-record-formfor standard record operations without Apex - Navigation:
NavigationMixinfor navigating between records and pages - Lightning Message Service (LMS): Communication between components that don't share a parent
- Jest Testing: Unit test your LWC JavaScript logic without deploying
Want to go deeper on LWC? Book a hands-on session with ApexSensei and I'll walk you through building a real-world LWC component for your specific use case. Start practicing on ApexSensei โ
What's your first LWC project going to be? Tell me in the comments and I'll give you pointers. ๐ฌ