Contact Explorer with Salesforce LWC
Build a searchable contact component on Salesforce Account pages using LWC and Apex.
Introduction
30 Second Summary
Every sales team runs on relationships. When a rep opens a company's page in their CRM, they need instant access to every contact at that company without extra clicks or tab-switching.
In this project, you will build a custom Salesforce Lightning Web Component that lives on Account record pages. It pulls live contact data through an Apex controller, displays it in a searchable card layout, and deploys through the full Salesforce DX workflow.
What You'll Build
Open any Account in your Salesforce org and your Contact Explorer component instantly shows that account's contacts. Names, titles, emails, and phone numbers appear in professional cards you can search in real-time.
By the end of this project, you'll have:
- A live contact list that appears on any Account page, pulling name, title, email, and phone from your Salesforce database through an Apex controller wired to your component.
- A real-time search box where typing instantly filters the contact list by name, email, or title.
- Professional contact cards styled with SLDS and interactive Lightning base components for clickable emails and phone numbers.
- Secret Mission: Add a click-to-view contact detail panel that fetches individual record data using the getRecord wire adapter from lightning/uiRecordApi.
Do I need Salesforce experience?
No. This project assumes strong frontend development experience with JavaScript, HTML, and CSS. You will learn Salesforce-specific concepts like LWC, Apex, and the wire service as you build.
Before We Start
Before you write any code, take a moment to lock in what you are about to build and why it matters.
Set Up Your Salesforce Development Environment
Every Salesforce project needs a live org to deploy code to. An org is your cloud workspace where your components run, your data lives, and your users interact with your app.
To build your Lightning Web Component, you also need local tools to write code and push it to that org. In this step, you'll create your free Salesforce Developer Edition org, install the Salesforce CLI, and add the Salesforce Extension Pack to VS Code.
In this step, get ready to:
- Sign up for a free Salesforce Developer Edition org.
- Install and verify the Salesforce CLI on Windows.
- Install the Salesforce Extension Pack in VS Code.
Sign up for a Salesforce Developer Edition org
A Developer Edition org is a free, full-featured Salesforce environment for building and testing. You'll use it as your deployment target throughout this project.
- Open your browser and go to the Salesforce Developer signup page.
- Fill in your First Name, Last Name, Email, and Company fields.
- In the Username field, enter a unique value formatted like an email address (e.g., yourname@lwc-dev.com). This does not need to be a real email address. It is your login ID for Salesforce.
- Complete the CAPTCHA.
- Click Sign Me Up.
What is a Developer Edition org?
A Developer Edition org gives you full access to the Salesforce platform at no cost. You can build custom components, write Apex code, and configure the entire platform just like a production org.
Your org does not expire as long as you log in at least once every 6 months.
- Check your email inbox for the activation email from Salesforce.
- Click the activation link in the email.
- Set your password and security question when prompted.
- Log in to your new Salesforce org.
After logging in, you should see the Salesforce Setup home page with a navigation menu on the left and guided setup prompts in the center.
Didn't receive the activation email?
- Check your spam or junk folder.
- Make sure you entered a valid personal email address during signup. The Email field must be a real email you can access. The Username field is separate and does not need to be real.
- Help me troubleshoot my Salesforce Developer Edition signup.
Install Salesforce CLI on Windows
The Salesforce CLI is a command-line tool that lets you create projects, deploy code, and manage your orgs directly from the terminal.
- Open your browser and go to the Salesforce CLI download page.
- Click Download for Windows x64 to download the installer.
- Run the downloaded installer and follow the prompts to complete installation.
- Close VS Code if it is currently open.
Why do I need to reopen VS Code?
The Salesforce CLI installer adds the sf command to your system PATH. Any terminal windows that were open before the install will not recognize the new command. Reopening VS Code refreshes its integrated terminal so it can find sf.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick).
Before you run the next command, what do you expect to see if the installation succeeded?
- Verify the Salesforce CLI is installed by running this command:
sf --version
✔️ I see a version number
The Salesforce CLI is installed and ready. You should see output starting with @salesforce/cli/ followed by a version number.
ⓧ Command not found or not recognized
- Make sure you closed and reopened VS Code after running the installer.
- Try opening a fresh terminal in VS Code by clicking Terminal in the menu bar, then New Terminal.
- If it still does not work, restart your computer to ensure the PATH changes take effect.
- Help me fix Salesforce CLI not being recognized in my terminal.
Use the built-in Windows terminal
The Salesforce CLI works best with native Windows terminals like cmd.exe or PowerShell. Avoid using WSL, Cygwin, or MinGW for Salesforce development.
Install the Salesforce Extension Pack in VS Code
The Salesforce Extension Pack adds LWC IntelliSense, Apex syntax highlighting, deployment commands, and SFDX command palette integration to VS Code.
- In VS Code, press Ctrl+Shift+X to open the Extensions view.
- Type Salesforce Extension Pack in the search bar.
- Find the extension published by Salesforce and click Install.
After installation completes, you should see the Salesforce Extension Pack listed in your installed extensions.
Extension not installing?
- Make sure your VS Code is version 1.90 or later. Check by clicking Help in the menu bar, then About.
- Check your internet connection and try the search again.
- Help me install the Salesforce Extension Pack in VS Code.
Your development environment is ready. In the next step, you'll create your first Lightning Web Component and see it live in your Salesforce org.
Create Your First Lightning Web Component
Your Salesforce development environment is locked in. You have a Developer Edition org, the Salesforce CLI, and VS Code with the Salesforce Extension Pack.
Before wiring up live data, you need to prove the full development loop works end to end. In this step, you'll create a Salesforce DX project, scaffold your first Lightning Web Component, and deploy it to your org to see it live on a page.
In this step, get ready to:
- Create a Salesforce DX project and authorize your Developer Edition org.
- Generate and customize the contactExplorer LWC component.
- Deploy the component and add it to the Sales Home page.
Create a Salesforce DX project and authorize your org
A Salesforce DX project is the local folder structure that holds all your component source code, metadata, and configuration. You need one before you can build or deploy anything.
- Open the terminal in VS Code by pressing Ctrl+`.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Salesforce DX project by running this command:
sf project generate --name contact-explorer
What does this command do?
The sf project generate command scaffolds a complete Salesforce DX project folder. It creates sfdx-project.json (the project config), a force-app/ directory (where your components and classes live), and several dotfiles for formatting and Git.
- Confirm the project folder was created by running:
ls
You should see contact-explorer listed in the output.
- Move into your new project folder by running:
cd contact-explorer
Now you need to connect this local project to your Salesforce org so you can deploy code to it.
- Authorize your Developer Edition org by running this command:
sf org login web --set-default --alias DevOrg
What does this command do?
The --set-default flag makes this org the default target for all future CLI commands. The --alias DevOrg flag gives it a short, memorable name so you can reference it easily later.
A browser window will open
This command opens a Salesforce login page in your browser. Log in with your Developer Edition credentials (the username and password you set up in Step 1). After logging in, click Allow to grant the CLI access to your org.
After you click Allow, switch back to VS Code. You should see a success message in the terminal that includes your username and org ID.
Browser didn't open?
If no browser window appeared, copy the URL from the terminal output and paste it into your browser manually. Make sure you are logging in with your Developer Edition credentials, not a production Salesforce account.
Help me troubleshoot the sf org login web command.
Generate the LWC component scaffold
With your project connected to your org, you can generate your first component. The Salesforce CLI creates the boilerplate files every LWC needs.
- Generate the contactExplorer component by running this command:
sf lightning generate component --type lwc --name contactExplorer --output-dir force-app/main/default/lwc
What did this create?
This command created three files inside force-app/main/default/lwc/contactExplorer/:
- contactExplorer.html is the component's template (its visual structure).
- contactExplorer.js is the component's JavaScript controller (its logic).
- contactExplorer.js-meta.xml is the component's configuration (where it can appear and its API version).
- Confirm the files were created by running:
ls force-app/main/default/lwc/contactExplorer/
You should see contactExplorer.html, contactExplorer.js, and contactExplorer.js-meta.xml listed.
The scaffold gives you an empty template and a minimal JavaScript class. You need to add some placeholder content and configure the component to appear in Lightning App Builder.
- In the VS Code Explorer sidebar, expand force-app > main > default > lwc > contactExplorer.
- Open contactExplorer.html and replace all of its content with the following:
<template>
<lightning-card title="Contact Explorer" icon-name="standard:contact">
<div class="slds-m-around_medium">
<p>Hello, Salesforce!</p>
</div>
</lightning-card>
</template>
What does this template do?
The lightning-card is a built-in Salesforce base component that renders a styled card container with a title and icon. The slds-m-around_medium class adds medium-sized margin from the SLDS (Salesforce Lightning Design System).
- Save contactExplorer.html.
- Now open contactExplorer.js-meta.xml and replace all of its content with the following:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>67.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
Why does this configuration matter?
The isExposed flag set to true makes your component visible in Lightning App Builder. Without it, Salesforce hides the component from the drag-and-drop interface.
The lightning__HomePage target tells Salesforce this component is allowed on Home pages. You'll change this to lightning__RecordPage in a later step when you move the component to Account pages.
- Save contactExplorer.js-meta.xml.
No changes needed for the JS file
The scaffold-generated contactExplorer.js already contains the minimal class definition your component needs. You'll add logic to it in later steps.
✔️ Awesome, I've got everything!
Make sure you've saved both contactExplorer.html and contactExplorer.js-meta.xml.
ⓧ I'd like to double check the full code
Here are all three component files as they should look right now.
contactExplorer.html:
<template>
<lightning-card title="Contact Explorer" icon-name="standard:contact">
<div class="slds-m-around_medium">
<p>Hello, Salesforce!</p>
</div>
</lightning-card>
</template>
contactExplorer.js:
import { LightningElement } from 'lwc';
export default class ContactExplorer extends LightningElement {}
contactExplorer.js-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>67.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
Deploy and add the component to the Sales Home page
Your component exists locally but Salesforce doesn't know about it yet. Deploying pushes your local source code to the org so it becomes available in the Lightning App Builder.
- Deploy all your project source to your org by running this command:
sf project deploy start --source-dir force-app
What does this command do?
The sf project deploy start command uploads everything in the force-app directory to your connected Salesforce org. The terminal shows a progress indicator followed by a table of deployed components.
You should see a table listing your deployed components with a status of Changed or Created. The deploy should complete without errors.
Deploy failed?
Double-check that isExposed is set to true in your contactExplorer.js-meta.xml file. Also confirm you saved both edited files before running the deploy command.
Help me troubleshoot my Salesforce deployment.
Now you need to add the component to a page in your org so you can see it live.
- Open your Salesforce org in the browser by running:
sf org open
What is sf org open?
This command opens your default org's home page in the browser. Since you set DevOrg as the default earlier, it opens that org automatically.
- In the Salesforce browser tab, click the App Launcher (the grid of nine dots in the top-left corner).
- Search for and select Sales to navigate to the Sales app.
- Click the Home tab in the navigation bar at the top.
- Click the gear icon in the top-right corner of the page.
- Select Edit Page from the dropdown menu. This opens the Lightning App Builder.
What is the Lightning App Builder?
Lightning App Builder is Salesforce's drag-and-drop page editor. It lets you add, remove, and rearrange components on any Lightning page without writing code. Your deployed LWC component appears in the Custom section of the components panel on the left.
- In the components panel on the left side, scroll down to find the Custom section.
- Find contactExplorer in the Custom section and drag it onto the page canvas.
- Click Save in the top-right corner of the App Builder.
- Click Activate when the prompt appears.
- Select Assign as Org Default to make this the default Home page for all users.
- Click Save to confirm the activation.
- Click the Back arrow in the top-left corner to exit Lightning App Builder and return to the Home page.
Before you look at the page, take a moment to predict what you'll see. What content did you put in your component's HTML template?
You should now see a Contact Explorer card on the Sales Home page with the text "Hello, Salesforce!" inside it. That card is your deployed Lightning Web Component running live in your org.
Don't see your component on the page?
If the contactExplorer card isn't visible, go back to the gear icon and select Edit Page. Confirm the component is on the canvas. Also verify that you clicked Activate and Assign as Org Default after saving.
If the component doesn't appear in the Custom section of Lightning App Builder, the deployment may have failed silently. Re-run sf project deploy start --source-dir force-app and check the terminal output for errors.
Help me find my component in Lightning App Builder.
Your first Lightning Web Component is deployed and live. Next up, you'll replace that placeholder text with real contact data pulled from your Salesforce org using an Apex controller.
Connect to Live Salesforce Data with Apex
Your Contact Explorer is live on the Sales Home page, but right now it just says "Hello, Salesforce!" A static greeting does not prove anything about your Salesforce development skills.
In this step, you will create an Apex controller that queries real Contact records from the Salesforce database and wire that data directly into your LWC. By the end, your component will display actual contact information pulled from your org.
In this step, get ready to:
- Create sample Contact records in your Salesforce org.
- Build an Apex controller that queries Contact data with SOQL.
- Wire the Apex data into your LWC to display live contacts.
Create sample contacts in your org
Your Developer Edition org starts mostly empty. You need Contact records in the database before your component has anything to display.
- Switch back to your Salesforce org in the browser.
- Click the App Launcher grid icon (nine dots) in the top-left corner.
- Type Contacts in the search field and select the Contacts tab.
- Click New to create a new contact.
- Fill in the First Name, Last Name, Title, Email, and Phone fields with test data.
- In the Account Name field, search for and select an existing account such as Edge Communications or Burlington Textiles.
- Click Save.
- Repeat to create 3-4 contacts total, assigning them all to the same account.
You should now see all your new contacts listed in the Contacts tab.
Why create sample data?
Developer Edition orgs do not come pre-loaded with Contact records. Without contacts in the database, your component would query an empty table and display nothing.
Create the Apex controller
Apex is Salesforce's server-side programming language. Your controller will run a SOQL query to fetch Contact records and expose the results to your Lightning Web Component.
- Switch back to VS Code.
- Open the Command Palette by pressing Ctrl+Shift+P.
- Type SFDX: Create Apex Class and select it.
- Enter ContactController as the class name and press Enter.
- Accept the default directory location (force-app/main/default/classes).
VS Code creates two files: ContactController.cls and ContactController.cls-meta.xml. The .cls file opens automatically with a skeleton class.
- Replace all the generated code in ContactController.cls with this:
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts() {
return [
SELECT Id, Name, Title, Email, Phone
FROM Contact
ORDER BY Name
];
}
}
What does this code do?
- public with sharing class means the class respects the current user's data-sharing rules. Only records the user is allowed to see are returned.
- @AuraEnabled(cacheable=true) exposes the method to Lightning Web Components and enables client-side caching for better performance.
- The SOQL query selects Id, Name, Title, Email, and Phone from the Contact object, ordered alphabetically by Name.
- Save ContactController.cls.
- Deploy the Apex class to your org by running this command in the VS Code terminal:
sf project deploy start --source-dir force-app
The terminal should show a successful deployment with no errors.
Seeing a deployment error?
- Check that the class name in your code (ContactController) matches the filename ContactController.cls exactly. Apex class names are case-sensitive.
- Make sure you saved the file before deploying. Unsaved changes are not included in the deployment.
- Confirm you are running the deploy command from within your contact-explorer project folder.
Still stuck? Help me debug my Apex deployment error.
✔️ Awesome, I've got everything!
Great. Make sure you saved ContactController.cls before moving on.
ⓧ I'd like to double check the full code
Here is the complete ContactController.cls file:
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts() {
return [
SELECT Id, Name, Title, Email, Phone
FROM Contact
ORDER BY Name
];
}
}
Wire the Apex method to your LWC
Your Apex controller is deployed and ready. Now you need to connect it to your component so the contact data flows from the server into the UI.
The Wire Service is how LWC components call Apex methods. The @wire decorator tells the framework to automatically call your method when the component loads and store the result in a property.
- Open contactExplorer.js and replace all the code with this:
import { LightningElement, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class ContactExplorer extends LightningElement {
@wire(getContacts)
contacts;
}
What does this code do?
- The first import pulls in wire from the lwc module, giving you access to the @wire decorator.
- The second import maps getContacts to your Apex method at ContactController.getContacts.
- @wire(getContacts) automatically calls the Apex method and stores the result in the contacts property. The property has a .data field for successful results and an .error field for failures.
- Save contactExplorer.js.
- Open contactExplorer.html and replace all the code with this:
<template>
<lightning-card title="Contact Explorer" icon-name="standard:contact">
<template lwc:if={contacts.data}>
<div class="slds-m-around_medium">
<template for:each={contacts.data} for:item="contact">
<div key={contact.Id}>
<p>{contact.Name}</p>
<p>{contact.Email}</p>
</div>
</template>
</div>
</template>
<template lwc:if={contacts.error}>
<div class="slds-m-around_medium slds-text-color_error">
<p>Error loading contacts.</p>
</div>
</template>
</lightning-card>
</template>
What does this template do?
- lwc:if={contacts.data} only renders the contact list when the Apex call returns data successfully.
- for:each={contacts.data} for:item="contact" iterates over each Contact record. The key={contact.Id} attribute is required so LWC can efficiently track and re-render each item.
- {contact.Name} and {contact.Email} display each contact's name and email inline.
- The second lwc:if={contacts.error} block shows an error message if the Apex call fails for any reason.
- Save both contactExplorer.js and contactExplorer.html.
- Deploy all the changes by running this command:
sf project deploy start --source-dir force-app
Before you check the result, what do you think the Sales Home page will show now? Will it still say "Hello, Salesforce!" or something different?
- Switch back to your Salesforce org and refresh the Sales Home page.
You should see a list of your contact names and emails pulled live from your Salesforce org, displayed inside the Contact Explorer card.
Not seeing contacts on the page?
- Check that the terminal showed a successful deployment. If the deploy failed, fix the error and deploy again.
- Confirm you created sample Contact records in the earlier substep. Without records in the database, the list will be empty.
- Verify that contactExplorer.js imports from @salesforce/apex/ContactController.getContacts. A typo in the import path will silently fail.
- Try a hard refresh (Ctrl+Shift+R) to clear cached page content.
Still stuck? Help me debug why my Contact Explorer is not showing any contacts.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved and your deployment completed successfully.
ⓧ I'd like to double check the full code
Here is the complete contactExplorer.js:
import { LightningElement, wire } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
export default class ContactExplorer extends LightningElement {
@wire(getContacts)
contacts;
}
Here is the complete contactExplorer.html:
<template>
<lightning-card title="Contact Explorer" icon-name="standard:contact">
<template lwc:if={contacts.data}>
<div class="slds-m-around_medium">
<template for:each={contacts.data} for:item="contact">
<div key={contact.Id}>
<p>{contact.Name}</p>
<p>{contact.Email}</p>
</div>
</template>
</div>
</template>
<template lwc:if={contacts.error}>
<div class="slds-m-around_medium slds-text-color_error">
<p>Error loading contacts.</p>
</div>
</template>
</lightning-card>
</template>
Your component is now pulling live data from Salesforce. Next, you will make it context-aware so it shows only the contacts that belong to the Account the user is currently viewing.
Make the Component Context-Aware
Your component is pulling live contact data straight from Salesforce. Every contact in your org flows into the card on the Sales Home page.
The problem is that the component shows every single contact, regardless of which page it sits on. When a sales rep opens an Account, they need to see only that Account's contacts. In this step, you'll wire the component to the page's record ID so it automatically filters by Account.
In this step, get ready to:
- Move the component from the Home page to Account record pages.
- See what happens when the component has no page context.
- Add the recordId property and update the Apex query to filter contacts by Account.
Move the component target to Record Pages
The contactExplorer.js-meta.xml file controls where your component can appear in Salesforce. Right now, it targets the Home page. You need to change it to target record pages instead.
- In VS Code, open contactExplorer.js-meta.xml in the force-app/main/default/lwc/contactExplorer/ folder.
- Find this line:
<target>lightning__HomePage</target>
- Replace lightning__HomePage with lightning__RecordPage, so the line reads:
<target>lightning__RecordPage</target>
- Save contactExplorer.js-meta.xml.
- Deploy the change by running this command in the VS Code terminal:
sf project deploy start --source-dir force-app
- You should see a status of Succeeded in the terminal output confirming the deploy completed.
Deploy failed?
Make sure you saved contactExplorer.js-meta.xml before running the deploy command. Unsaved changes are not included in the deployment.
Double-check that the target value is spelled exactly lightning__RecordPage with two underscores and a capital R and P.
help me fix my Salesforce deploy failure
Add the component to an Account record page
The component now targets record pages instead of the Home page. Let's place it on an Account record page using Lightning App Builder and see how it behaves.
- Switch back to your Salesforce org in the browser.
- Click the App Launcher (the grid icon in the top-left corner).
- Search for and select Accounts.
- Click on any Account that has contacts (like Edge Communications or the Account you assigned contacts to in Step 3).
- Click the gear icon in the top-right corner of the page.
- Select Edit Page.
- In the Lightning App Builder sidebar on the left, scroll down to the Custom section.
- Drag contactExplorer onto the page canvas.
- Click Save.
- Click Activation.
- Click Assign as Org Default.
- Click Save in the dialog to confirm.
Before you leave the App Builder, take a moment to predict. What contacts do you think the component will display on this Account page? Only this Account's contacts, or every contact in your org?
- Click the back arrow at the top of Lightning App Builder to return to the Account record page.
Your component shows every single contact in your org. Not just this Account's contacts. Navigate to a different Account page and you'll see the exact same list.
Why does this happen?
The component calls getContacts, which runs a SOQL query with no WHERE clause. It returns every contact in the database.
The component has no idea which Account page it's sitting on. Salesforce provides page context through a property called recordId, but your component doesn't use it yet. That's what you'll fix next.
Add the recordId and update the Apex query
Two files need to change. The Apex controller needs a parameter to accept an Account ID and filter the query. The JavaScript file needs to receive the page's record ID and pass it to the Apex method.
- In VS Code, open ContactController.cls in the force-app/main/default/classes/ folder.
- Replace the entire file contents with this updated controller:
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContactsByAccountId(Id accountId) {
// Query only contacts belonging to the given account
return [
SELECT Id, Name, Title, Email, Phone
FROM Contact
WHERE AccountId = :accountId
ORDER BY Name
];
}
}
What changed in the Apex controller?
- The method name changed from getContacts to getContactsByAccountId to reflect its new, filtered purpose.
- The method now accepts an Id accountId parameter so it knows which Account to filter for.
- The SOQL query includes WHERE AccountId = :accountId, returning only contacts belonging to that specific Account.
- Save ContactController.cls.
- Now open contactExplorer.js in the force-app/main/default/lwc/contactExplorer/ folder.
- Replace the entire file contents with this updated JavaScript:
import { LightningElement, api, wire } from 'lwc';
import getContactsByAccountId from '@salesforce/apex/ContactController.getContactsByAccountId';
export default class ContactExplorer extends LightningElement {
// Receives the current record's ID from the page context
@api recordId;
// Pass recordId reactively to the Apex method
@wire(getContactsByAccountId, { accountId: '$recordId' })
contacts;
}
What changed in the JavaScript?
- The import from lwc now includes api alongside LightningElement and wire.
- The import path changed from getContacts to getContactsByAccountId to match the renamed Apex method.
- @api recordId is a public reactive property. Salesforce automatically populates it with the current record's ID when the component sits on a record page.
- The @wire decorator passes { accountId: '$recordId' } to the Apex method. The $ prefix makes recordId reactive. Whenever the learner navigates to a different Account, recordId updates and the wire re-fires the Apex call automatically.
- Save contactExplorer.js.
- Deploy all changes by running this command:
sf project deploy start --source-dir force-app
Before you check the Account page, what do you expect to see this time? Will the component still show every contact, or only the ones belonging to this Account?
- Switch back to your Salesforce org in the browser.
- Refresh the Account record page you were viewing earlier.
- You should see only the contacts that belong to this specific Account.
- Navigate to a different Account that also has contacts.
- Confirm that the contact list changes to show only that Account's contacts.
Still seeing all contacts?
Make sure the Apex method name in ContactController.cls matches the import path in contactExplorer.js. Both must say getContactsByAccountId.
Check that the @wire decorator uses a $ prefix on recordId like this: '$recordId'. Without the $, the wire treats it as a static string instead of a reactive reference.
Confirm you saved both files and re-ran sf project deploy start --source-dir force-app after making changes.
help me debug why my LWC still shows all contacts instead of filtering by Account
✔️ Awesome, I've got everything!
Great. Double-check that you've saved all three files: contactExplorer.js-meta.xml, ContactController.cls, and contactExplorer.js.
ⓧ I'd like to double check the full code
Here are all three files that changed in this step. Compare each one against yours.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>67.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContactsByAccountId(Id accountId) {
return [
SELECT Id, Name, Title, Email, Phone
FROM Contact
WHERE AccountId = :accountId
ORDER BY Name
];
}
}
import { LightningElement, api, wire } from 'lwc';
import getContactsByAccountId from '@salesforce/apex/ContactController.getContactsByAccountId';
export default class ContactExplorer extends LightningElement {
@api recordId;
@wire(getContactsByAccountId, { accountId: '$recordId' })
contacts;
}
Your component now knows exactly which Account page it belongs to. Each Account shows only its own contacts. Next up, you'll add instant search filtering and professional styling to make the component genuinely useful for a sales rep scanning through contacts.
Add Search and Polish the UI
Your component is now context-aware. Each Account page shows only its own related contacts, pulled live from Salesforce through your Apex controller.
A plain list of names is hard to scan when an account has dozens of contacts. In this step, you will add instant search filtering and professional styling so the component is genuinely useful for sales reps.
In this step, get ready to:
- Add real-time search filtering to the contact list.
- Create professional SLDS-styled contact cards.
- Deploy and verify the polished component on an Account page.
Add the search input
Right now the component dumps every contact in a flat list. You need a search box that filters the list as the user types, without making another server call.
The approach is client-side filtering. Your @wire already fetches all contacts for the account. A JavaScript getter will filter that data in memory whenever the search term changes.
- In VS Code, open contactExplorer.js inside force-app/main/default/lwc/contactExplorer/.
- Find the line @api recordId; and add this new property directly below it:
searchTerm = "";
- Next, find the line contacts; (the end of the @wire declaration). Copy the following code below it:
// Filter contacts by matching the search term against Name, Email, and Title
get filteredContacts() {
if (!this.contacts.data) {
return [];
}
if (!this.searchTerm) {
return this.contacts.data;
}
const term = this.searchTerm.toLowerCase();
return this.contacts.data.filter(
(contact) =>
contact.Name.toLowerCase().includes(term) ||
(contact.Email && contact.Email.toLowerCase().includes(term)) ||
(contact.Title && contact.Title.toLowerCase().includes(term))
);
}
// Check whether any contacts match the current filter
get hasContacts() {
return this.filteredContacts.length > 0;
}
// Update searchTerm when the user types in the search box
handleSearchChange(event) {
this.searchTerm = event.target.value;
}
What does this code do?
The filteredContacts getter is a reactive computed property. Every time searchTerm changes, LWC re-evaluates the getter and re-renders the template automatically.
- If the user has not typed anything, the getter returns the full contacts.data array unchanged.
- Otherwise it filters by checking whether Name, Email, or Title contain the lowercase search term.
- The hasContacts getter returns true when at least one contact matches, so the template can show an empty-state message when nothing matches.
- Save contactExplorer.js.
✔️ Awesome, I've got everything!
Great. Double check you saved contactExplorer.js.
ⓧ I'd like to double check the full code
Your contactExplorer.js should now have, in order:
- The two import lines at the top.
- The class declaration with @api recordId; and searchTerm = ""; properties.
- The @wire decorator and contacts property.
- The filteredContacts getter (15 lines).
- The hasContacts getter (3 lines).
- The handleSearchChange method (3 lines).
- The closing } brace.
Now update the HTML template to use the new search logic. The template needs a search input at the top and must iterate over filteredContacts instead of contacts.data.
- Open contactExplorer.html.
- Find the opening <lightning-card> tag. Replace everything between it and the <template lwc:if={contacts.error}> line with the following code. Keep the error template and closing tags as they are:
<!-- Search input bound to the searchTerm property -->
<div class="slds-m-around_medium">
<lightning-input
type="search"
label="Search Contacts"
value={searchTerm}
onchange={handleSearchChange}
placeholder="Search by name, email, or title..."
></lightning-input>
</div>
<template lwc:if={contacts.data}>
<!-- Only render the list when filtered results exist -->
<template lwc:if={hasContacts}>
<div class="slds-m-around_medium">
<template for:each={filteredContacts} for:item="contact">
<div key={contact.Id}>
<p>{contact.Name}</p>
<p>{contact.Email}</p>
</div>
</template>
</div>
</template>
<!-- Empty state when search returns no matches -->
<template lwc:else>
<div class="slds-m-around_medium slds-text-align_center">
<p>No contacts found.</p>
</div>
</template>
</template>
What changed in the template?
- A lightning-input with type="search" sits above the contact list. It binds its value to searchTerm and fires handleSearchChange on every keystroke.
- The iteration now loops over filteredContacts instead of contacts.data, so only matching contacts render.
- A lwc:if={hasContacts} guard shows a "No contacts found." message when the search term matches nothing.
- Save contactExplorer.html.
- Deploy the updated component by running this command in the VS Code terminal:
sf project deploy start --source-dir force-app
Before you check the result, predict what you expect to see on the Account page. Will the search box appear? Will the contacts still show?
- Switch to your Salesforce org and navigate to an Account record page that has contacts.
- Refresh the page. You should see a search box labelled "Search Contacts" at the top of the Contact Explorer card, with the contact list still visible below it.
- Type part of a contact's name into the search box. The list filters in real-time, showing only matching contacts.
- Type a term that matches no contacts. You should see the "No contacts found." message.
Search box not appearing?
- Check that the deploy command completed without errors. Look for "Deploy Succeeded" in the terminal output.
- Make sure you saved both contactExplorer.js and contactExplorer.html before deploying.
- Confirm the lightning-input block sits inside the <lightning-card> tags but outside the <template lwc:if={contacts.data}> block.
Still stuck? Why isn't my lightning-input search box rendering in the Contact Explorer?
Style the contact cards with SLDS
Search works, but the contact cards are still unstyled paragraphs. You will now give each card a professional two-column layout using SLDS utility classes and Lightning base components.
- In contactExplorer.html, find the <div key={contact.Id}> block inside the for:each loop. It currently looks like this:
<div key={contact.Id}>
<p>{contact.Name}</p>
<p>{contact.Email}</p>
</div>
- Replace that entire <div key={contact.Id}> block (including its children) with the following SLDS grid layout:
<!-- Contact card with SLDS grid layout -->
<div key={contact.Id} class="contact-card slds-p-around_small slds-m-bottom_x-small">
<div class="slds-grid slds-grid_vertical-align-center">
<!-- Left column: name and title -->
<div class="slds-col">
<p class="contact-name">{contact.Name}</p>
<template lwc:if={contact.Title}>
<p class="contact-detail">{contact.Title}</p>
</template>
</div>
<!-- Right column: email and phone -->
<div class="slds-col slds-text-align_right">
<template lwc:if={contact.Email}>
<p>
<lightning-formatted-email
value={contact.Email}
></lightning-formatted-email>
</p>
</template>
<template lwc:if={contact.Phone}>
<p>
<lightning-formatted-phone
value={contact.Phone}
></lightning-formatted-phone>
</p>
</template>
</div>
</div>
</div>
What does this layout do?
- The slds-grid class creates a flexbox row. The left slds-col holds the contact's name and title. The right slds-col holds email and phone, right-aligned.
- The lightning-formatted-email component renders the email as a clickable mailto: link. The lightning-formatted-phone component renders the phone number as a clickable tel: link.
- Each field is wrapped in lwc:if so the card does not show empty lines when data is missing.
- Save contactExplorer.html.
✔️ Awesome, I've got everything!
Great. Double check you saved contactExplorer.html.
ⓧ I'd like to double check the full code
Your contactExplorer.html should now have, in order:
- The opening <template> and <lightning-card> tags.
- The search input <div> with lightning-input (10 lines).
- The contacts.data template with the hasContacts guard.
- The for:each={filteredContacts} loop containing the SLDS grid contact card.
- The empty state (lwc:else) with "No contacts found." text.
- The error template (contacts.error) at the bottom.
- The closing </lightning-card> and </template> tags.
The SLDS utility classes handle layout, but you still need custom CSS for the card backgrounds and typography.
- Create a new file called contactExplorer.css in the same folder as your other component files (force-app/main/default/lwc/contactExplorer/).
- Add the following styles to contactExplorer.css:
/* Card container styling */
.contact-card {
border: 1px solid #e5e5e5;
border-radius: 0.25rem;
background-color: #fafaf9;
}
/* Contact name emphasis */
.contact-name {
font-weight: 700;
font-size: 0.875rem;
color: #080707;
}
/* Subtle styling for secondary contact info */
.contact-detail {
font-size: 0.75rem;
color: #706e6b;
}
How does CSS work in LWC?
LWC automatically scopes CSS to the component. Any styles in contactExplorer.css only apply inside your contactExplorer component. They will not leak into other components on the page.
The .contact-card class gives each card a subtle border and warm background. The .contact-name and .contact-detail classes create visual hierarchy between the contact's name and their title.
The colour values (#e5e5e5, #fafaf9, #080707, #706e6b) are drawn from the SLDS colour palette to match Salesforce's native look.
- Save contactExplorer.css.
✔️ Awesome, I've got everything!
Great. Double check you saved contactExplorer.css.
ⓧ I'd like to double check the full code
.contact-card {
border: 1px solid #e5e5e5;
border-radius: 0.25rem;
background-color: #fafaf9;
}
.contact-name {
font-weight: 700;
font-size: 0.875rem;
color: #080707;
}
.contact-detail {
font-size: 0.75rem;
color: #706e6b;
}
Deploy and verify the final component
All three files are updated. Time to deploy everything and see the polished component in action.
- Deploy the full project by running this command:
sf project deploy start --source-dir force-app
Before you refresh, predict what the contact cards will look like now. Will you see borders? Two columns? Clickable email addresses?
- Switch to your Salesforce org and navigate to an Account record page with several contacts.
- Refresh the page. You should see polished contact cards with a subtle border and warm background. Each card shows the contact's name and title on the left, with a clickable email and phone number on the right.
- Type a contact's name in the search box. The list should filter in real-time, showing only matching results.
- Click an email address. Your default mail app should open with the contact's address pre-filled.
- Clear the search box completely. All contacts should reappear.
Cards look unstyled or CSS not applying?
- Verify that contactExplorer.css is in the same folder as contactExplorer.html and contactExplorer.js. LWC matches CSS files by name and folder location.
- Check that the class name contact-card in your CSS matches class="contact-card ..." in your HTML exactly.
- Try a hard refresh (Ctrl+Shift+R on Windows) to clear the browser cache.
Still stuck? Why is my contactExplorer.css not being applied to the deployed LWC component?
Your Contact Explorer is polished and ready for real use. It pulls live data, filters instantly, and looks native inside Salesforce. Next up, the Secret Mission takes it even further with a click-to-view detail panel.
Secret mission
Add a Contact Detail Panel
Your Contact Explorer lists contacts, but clicking one does nothing. In this mission, you'll add a click-to-view detail panel that fetches individual contact data using the getRecord wire adapter from lightning/uiRecordApi. This introduces a second data access pattern that works without any Apex code.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on a free Salesforce Developer Edition org and local tools. There are no ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Salesforce Developer Edition org (with deployed LWC, Apex class, and sample contacts).
- Local contact-explorer project folder on your Desktop.
- Salesforce CLI installed on your machine.
- Salesforce Extension Pack installed in VS Code.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend your component soon.
- Your Developer Edition org does not expire as long as you log in at least once every 6 months.
- Your deployed contactExplorer component and ContactController Apex class persist between sessions.
- Salesforce CLI and the Salesforce Extension Pack remain installed on your machine for future projects.
Pause - I'll come back to this later
Simply stop working. Everything remains as-is for your next session.
- Your org, deployed components, and sample data will all be there when you return.
- Remember to log in to your Developer Edition org at least once every 6 months to keep it active.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- To deactivate your Salesforce Developer Edition org, stop logging in. Salesforce will deactivate it after 6 months of inactivity.
- Delete the local project folder by running this command in your terminal:
cd ~\Desktop
Remove-Item -Recurse -Force contact-explorer
- Uninstall Salesforce CLI by opening Settings on Windows, selecting Apps, then Installed apps. Search for Salesforce CLI, click the three-dot menu, and select Uninstall.
- Uninstall the Salesforce Extension Pack in VS Code by opening the Extensions view (Ctrl+Shift+X), searching for Salesforce Extension Pack, and clicking Uninstall.
Nice Work!
Nice Work!
You just built and deployed a custom Lightning Web Component on a live Salesforce org. Your Contact Explorer pulls real data from the platform, filters it in real-time, and sits right where a sales rep would use it.
You've learned how to:
- Set up a complete Salesforce DX development environment and deploy a Lightning Web Component to a Developer Edition org using the Salesforce CLI.
- Create an Apex controller with @AuraEnabled methods and connect it to your LWC using the @wire decorator to display live Salesforce data.
- Make your component context-aware with @api recordId, add client-side search filtering, and style everything with SLDS and Lightning base components.
- (Secret Mission) Use the getRecord wire adapter from lightning/uiRecordApi with @salesforce/schema imports to fetch individual record data without Apex.
Ready to quiz yourself?