Project: Account/Contact Management Without Apex
By the end of this lesson, you'll be able to:
- Build a working Account and Contact management interface using only Lightning Data Service
- Combine wire reads with imperative writes in one cohesive component
- Apply reactive refresh so the UI never shows stale data after a change
Prerequisites: "Reactive Wire Parameters and Refreshing Data"
What We're Building
An accountManager component that:
- Displays a list of Accounts (via
@wireand a SOQL-free UI API call — Module 20's capstone revisits combining this with Apex for more complex querying). - Lets the user select an Account to view its Contacts.
- Lets the user add a new Contact to the selected Account using
createRecord.
Every single piece of this uses Lightning Data Service — no Apex class anywhere.
Displaying the Selected Account's Details
import { LightningElement, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class AccountManager extends LightningElement {
selectedAccountId;
@wire(getRecord, { recordId: '$selectedAccountId', fields: [NAME_FIELD, INDUSTRY_FIELD] })
account;
get accountName() {
return getFieldValue(this.account.data, NAME_FIELD);
}
}
Using '$selectedAccountId' as a reactive parameter (Lesson 5) means simply changing selectedAccountId automatically re-fetches the newly selected Account — no manual refresh call needed for this part.
Adding a New Contact
import { createRecord } from 'lightning/uiRecordApi';
import CONTACT_OBJECT from '@salesforce/schema/Contact';
import FIRST_NAME_FIELD from '@salesforce/schema/Contact.FirstName';
import LAST_NAME_FIELD from '@salesforce/schema/Contact.LastName';
import ACCOUNT_FIELD from '@salesforce/schema/Contact.AccountId';
async handleAddContact() {
const recordInput = {
apiName: CONTACT_OBJECT.objectApiName,
fields: {
[FIRST_NAME_FIELD.fieldApiName]: this.newFirstName,
[LAST_NAME_FIELD.fieldApiName]: this.newLastName,
[ACCOUNT_FIELD.fieldApiName]: this.selectedAccountId,
},
};
try {
await createRecord(recordInput);
// refresh whatever component/wire lists this Account's Contacts
} catch (error) {
this.error = error;
}
}
Setting ACCOUNT_FIELD.fieldApiName (AccountId) on the new Contact is what associates it with the currently selected Account — plain relational data, no Apex needed to establish it.
What This Project Proves
This is the module's entire thesis, demonstrated: reading records (@wire/getRecord), writing new ones (createRecord), and correctly refreshing the UI afterward (reactive parameters and refreshApex) — a genuinely complete, production-viable CRUD interface with zero Apex classes to write, deploy, or test.
Exercise
Write the reactive @wire declaration for fetching Contacts related to the selected Account (assume a getContactsForAccount UI API-friendly wire adapter exists with a recordId parameter).
Show hint
Follow the same $-prefixed pattern used throughout this module.
Exercise
Challenge: after successfully creating a new Contact, the Contacts list doesn't show it yet. Explain why, and what to add.
Show hint
Recall the distinction between reactive parameters and refreshApex.
Project: Account/Contact Management Without Apex Quiz
My Notes
Log in to keep private notes on this lesson.
Questions about this lesson
No questions yet — be the first to ask.
Log in to ask a question about this lesson.
Summary
A hands-on capstone proving the whole module's thesis: real, working record management — read, create, update — entirely without a single line of Apex.