Advanced 35 min read

Project: Advanced Account Search Backed by Apex

By the end of this lesson, you'll be able to:

  • Build a reactive Apex-backed search component using @wire
  • Apply wrapper/DTO patterns to shape the search results
  • Handle errors gracefully and bulkify a follow-up bulk action

Prerequisites: "Governor Limits and Bulkification from LWC"

What We're Building

An advancedAccountSearch component that:

  1. Reactively searches Accounts by name as the user types, using @wire and a cacheable=true Apex method.
  2. Displays results using a wrapper class combining the Account with its open Opportunity count.
  3. Lets the user select multiple results and bulk-update their status in a single Apex call.

This is Module 6's Account/Contact project taken further — the search and aggregation logic genuinely needs Apex this time, per Lesson 1's decision checklist.

The Apex Controller

public with sharing class AccountSearchController {
    @AuraEnabled(cacheable=true)
    public static List<AccountResult> search(String searchTerm) {
        List<AccountResult> results = new List<AccountResult>();
        for (Account acc : [
            SELECT Id, Name, (SELECT Id FROM Opportunities WHERE IsClosed = false)
            FROM Account
            WHERE Name LIKE :('%' + searchTerm + '%')
            WITH SECURITY_ENFORCED
            LIMIT 25
        ]) {
            results.add(new AccountResult(acc.Id, acc.Name, acc.Opportunities.size()));
        }
        return results;
    }

    @AuraEnabled
    public static void bulkUpdateStatus(List<Id> accountIds, String status) {
        if (accountIds == null || accountIds.isEmpty()) {
            throw new AuraHandledException('Select at least one account.');
        }
        List<Account> accounts = [SELECT Id, Status__c FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED];
        for (Account acc : accounts) {
            acc.Status__c = status;
        }
        update accounts;
    }
}

public class AccountResult {
    @AuraEnabled public Id accountId;
    @AuraEnabled public String accountName;
    @AuraEnabled public Integer openOpportunityCount;

    public AccountResult(Id accountId, String accountName, Integer openOpportunityCount) {
        this.accountId = accountId;
        this.accountName = accountName;
        this.openOpportunityCount = openOpportunityCount;
    }
}

The Component

import { LightningElement, wire } from 'lwc';
import search from '@salesforce/apex/AccountSearchController.search';
import bulkUpdateStatus from '@salesforce/apex/AccountSearchController.bulkUpdateStatus';
import { refreshApex } from '@salesforce/apex';

export default class AdvancedAccountSearch extends LightningElement {
    searchTerm = '';
    selectedIds = [];
    wiredResults;

    @wire(search, { searchTerm: '$searchTerm' })
    wiredSearch(result) {
        this.wiredResults = result;
    }

    get results() {
        return this.wiredResults?.data ?? [];
    }

    get error() {
        return this.wiredResults?.error?.body?.message;
    }

    handleSearchTermChange(event) {
        this.searchTerm = event.target.value;
    }

    async handleBulkActivate() {
        try {
            await bulkUpdateStatus({ accountIds: this.selectedIds, status: 'Active' });
            await refreshApex(this.wiredResults);
        } catch (error) {
            this.error = error.body?.message ?? 'An unexpected error occurred.';
        }
    }
}

The '$searchTerm' reactive parameter (Lesson 3) re-runs the search automatically as the user types; the wrapper class (Lesson 4) keeps the result shape clean; AuraHandledException (Lesson 5) surfaces a real validation message; and bulkUpdateStatus (Lesson 6) updates every selected Account in one transaction rather than one call per row.

Exercise

Add a guard clause, as a comment, describing what should happen if searchTerm is empty, to avoid running an expensive LIKE '%%' query against every Account.

Show hint

Think about what the Apex method or the component could check before querying.

APEX

Exercise

Challenge: explain, as a comment, why bulkUpdateStatus is called imperatively rather than via @wire.

Show hint

Recall Lesson 3's decision table.

JAVASCRIPT

Project: Advanced Account Search Backed by Apex Quiz

1. Why is search() marked cacheable=true?

2. Why is bulkUpdateStatus called imperatively instead of via @wire?

3. What does the AccountResult wrapper class combine that a raw Account query alone could not?

4. What happens if bulkUpdateStatus is called with an empty list of Account Ids?

5. Why does bulkUpdateStatus update every selected Account in a single transaction rather than one call per Account?

Log in to submit the quiz and save your score.

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 combining every concept from this module: reactive Apex calls, wrapper classes, AuraHandledException error handling, and bulkified follow-up actions, into one realistic search-and-act component.