Introduction
When you build a Lightning Web Component that creates several records from user input, the instinct many developers bring from other web stacks is to call an Apex method inside a JavaScript loop. In Salesforce that is an anti-pattern, and it burns through governor limits and drags performance down. What follows is why that happens, how to restructure the code for bulk processing, and how to call Apex to create records without paying for it.
The anti-pattern: calling Apex in a loop
In ordinary web development, firing an API call per item in a list can be acceptable. In Salesforce, every @AuraEnabled method call starts a separate transaction. Iterate an array of objects in your JavaScript controller, call Apex for each one, and you have opened as many database transactions as you have items.
Why this fails:
- DML limits: Salesforce caps DML statements per transaction, currently 150. With 200 items in your loop, the code throws a
LimitException. - Network latency: every call costs a round trip between browser and Salesforce server, which is what makes the component feel sluggish.
- Atomicity: if the 50th record fails, you can be left with 49 records created and the rest lost, which makes error handling and data integrity nearly impossible to manage.
The solution: bulkifying via collections
Rather than calling Apex over and over, pass the collection to Apex once. Send the whole array of objects to a single Apex method and the DML runs bulkified, which keeps your transaction count low and your code inside governor limits.
Step 1: define the data structure
Have your LWC send a structured object, or a list of objects, that Apex can deserialize through @AuraEnabled.
Step 2: implement the Apex handler
Write a method that accepts a List of the sObject you intend to create.
public with sharing class RecordHandler {
@AuraEnabled
public static void createRecords(List<Account> accounts) {
if (accounts != null && !accounts.isEmpty()) {
try {
insert accounts;
} catch (DmlException e) {
throw new AuraHandledException('Error creating records: ' + e.getMessage());
}
}
}
}
Step 3: call from LWC
Build the full list in your JavaScript file before you call the method.
import { LightningElement } from 'lwc';
import createRecords from '@salesforce/apex/RecordHandler.createRecords';
handleSave() {
const records = [
{ sobjectType: 'Account', Name: 'Test 1' },
{ sobjectType: 'Account', Name: 'Test 2' }
];
createRecords({ accounts: records })
.then(() => {
// Handle success
})
.catch(error => {
// Handle error
});
}
Handling complex data relationships
Often you need parent-child records, an Account with several Contacts for example. Plain sObject lists will not carry that. Use a wrapper class in Apex for the nested structure instead.
The wrapper approach
public class AccountContactWrapper {
@AuraEnabled public String accountName;
@AuraEnabled public List<String> contactNames;
}
// In your Apex method:
@AuraEnabled
public static void processBulkData(List<AccountContactWrapper> wrappers) {
List<Account> accs = new List<Account>();
// Parse and insert...
}
The wrapper keeps business logic on the server side, so your LWC stays clean and the data transformation happens in a secure, server-side environment.
Best practices for production work
A few things to keep in mind before this ships:
- Client-side validation: validate data in the LWC before you send it to Apex. That stops pointless round trips for bad data.
AuraHandledException: throw one when Apex errors, so your JavaScript controller gets a message it can show.- Idempotency: consider checking whether a record already exists before you insert, to keep duplicates out.
- Atomic transactions: a single list means every record goes through one transaction, so if one fails you can roll the whole batch back and keep data integrity.
Leave a Comment