Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
A 3D architectural block structure illustrating the concept of calling Apex methods to create records in loops.
Apex

Calling Apex Methods to Create Records in Loops from LWC

Bulk record creation from a Lightning Web Component: why looping Apex calls is an anti-pattern, and what a bulkified version looks like.

Key takeaways Avoid the loop: never invoke @AuraEnabled methods inside a JavaScript loop. Bulkify: aggregate the data into one list or wrapper and pass it to a single Apex method call. Use DML well: a single insert or update on a list is significantly more efficient than individual calls. Use wrappers: for complex, nested data structures, use Apex wrapper classes to receive and process what the client sends. Data integrity: keeping the operations in a single method means record creation follows "all-or-nothing" ACID principles for the batch.

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.
Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment