Log Entry
GraphQL Mutations in LWC
If you have been using GraphQL queries in LWC already, Spring '26 feels like the missing piece finally arrived.
Until now, reads were elegant but writes usually bounced through Apex controllers. With GraphQL mutations now GA in API 66.0+, many standard CRUD paths can stay fully client-side in JavaScript.
That does not kill Apex. It does reduce how often you need it.
The shift in one view
For teams building a lot of form-driven apps, this is a meaningful architecture simplification.
Quick model: what mutation is in Salesforce GraphQL
Salesforce GraphQL now has two operation types:
Queryfor readsMutationfor create, update, delete
Mutation support is based on UI API, so the object must be UI API-supported.
LWC usage model: imperative only
Mutations are imperative in LWC.
import { LightningElement } from 'lwc';
import { gql, executeMutation } from 'lightning/graphql';
You do not use @wire for mutation execution.
A useful pattern is a tiny helper so every mutation has the same error behavior.
async function runMutation(query, variables, operationName) {
const response = await executeMutation({ query, variables, operationName });
if (response.errors?.length) {
const message = response.errors.map((e) => e.message).join('; ');
throw new Error(message);
}
return response.data;
}
If your team prefers explicit TypeScript typing, keep it tight and concrete:
import { gql, executeMutation } from 'lightning/graphql';
type MutationError = { message?: string };
type GraphqlDocument = ReturnType<typeof gql>;
interface CreateAccountResponse {
uiapi?: {
RecordCreate?: {
Record?: {
Id?: string | null;
} | null;
} | null;
} | null;
}
const CREATE_ACCOUNT_MUTATION: GraphqlDocument = gql`
mutation CreateAccount($record: RecordCreateRepresentationInput!) {
uiapi {
RecordCreate(input: { record: $record }) {
Record {
Id
}
}
}
}
`;
async function createAccount(name: string, phone?: string): Promise<string> {
const fields: Record<string, string> = { Name: name };
if (phone) fields.Phone = phone;
const { data, errors } = (await executeMutation({
operationName: 'CreateAccount',
query: CREATE_ACCOUNT_MUTATION,
variables: {
record: {
apiName: 'Account',
fields
}
}
})) as { data?: CreateAccountResponse; errors?: MutationError[] };
if (errors?.length) {
throw new Error(errors.map((e) => e.message).filter(Boolean).join('; '));
}
const createdId = data?.uiapi?.RecordCreate?.Record?.Id;
if (!createdId) throw new Error('No Id returned from mutation');
return createdId;
}
For Salesforce-hosted LWC, avoid imports from npm packages like graphql in component code. Stick to platform modules (lightning/*, @salesforce/*) and local modules.
Create example (custom object)
const CREATE_EXPENSE = gql`
mutation CreateExpense($input: Expense__cCreateInput!) {
uiapi {
Expense__cCreate(input: $input) {
Record {
Id
Name {
value
}
Description__c {
value
}
}
}
}
}
`;
async handleCreate() {
const data = await runMutation(CREATE_EXPENSE, {
input: {
Expense__c: {
Description__c: this.description
}
}
});
console.log('Created Id:', data.uiapi.Expense__cCreate.Record.Id);
}
Update example
const UPDATE_EXPENSE = gql`
mutation UpdateExpense($input: Expense__cUpdateInput!) {
uiapi {
Expense__cUpdate(input: $input) {
Record {
Id
Description__c {
value
}
Name {
value
}
}
}
}
}
`;
async handleUpdate() {
const data = await runMutation(UPDATE_EXPENSE, {
input: {
Id: this.expenseId,
Expense__c: {
Description__c: this.description
}
}
});
console.log('Updated:', data.uiapi.Expense__cUpdate.Record);
}
Delete example
const DELETE_EXPENSE = gql`
mutation DeleteExpense($input: Expense__cDeleteInput!) {
uiapi {
Expense__cDelete(input: $input) {
Id
}
}
}
`;
async handleDelete() {
const data = await runMutation(DELETE_EXPENSE, {
input: {
Id: this.expenseId
}
});
console.log('Deleted Id:', data.uiapi.Expense__cDelete.Id);
}
Multiple operations in one request (aliases + dependencies)
This is where GraphQL mutations become more than "just CRUD".
You can send multiple operations together, name them with aliases, and chain dependent operations using @{alias} references.
const MULTI_OP = gql`
mutation MultipleAccountOps {
uiapi(input: { allOrNone: false }) {
firstAcc: AccountCreate(
input: {
Account: {
Rating: "Hot"
}
}
) {
Record {
Id
}
}
secondAcc: AccountCreate(
input: {
Account: {
Name: "Account 2"
}
}
) {
Record {
Id
}
}
updateSecondAcc: AccountUpdate(
input: {
Id: "@{secondAcc}"
Account: {
Name: "Second Account"
}
}
) {
Record {
Id
Name {
value
}
}
}
}
}
`;
For this exact payload:
firstAccfails (Namerequired)secondAccsucceedsupdateSecondAccsucceeds because it depends onsecondAcc, not onfirstAcc
allOrNone controls transaction behavior
allOrNone defaults to true.
true: one failure rolls back everything.false: independent successful operations can still commit; failed operations (and dependents) fail.
Set this deliberately. Do not leave it implicit for business-critical writes.
Query + mutation together: full client-side loop
You can pair dynamic GraphQL queries (wired) with imperative mutations:
- query for initial/read state
- mutate on user action
- refresh or re-evaluate the query data
That gives you a full read/write model in one API style, without jumping between GraphQL reads and Apex writes.
What this reduces in day-to-day delivery
- less Apex maintenance for standard CRUD paths
- fewer server-side test classes for simple write endpoints
- smaller deployment blast radius for UI-driven record changes
- clearer frontend ownership of interaction workflows
For many teams, this is where development speed gains show up first.
What this does not replace
Use Apex when you need:
- complex business rules that should live server-side
- orchestration across objects/services with strict transaction semantics
- external integrations or callouts
- advanced custom error contracts beyond standard CRUD patterns
A practical rule: keep standard app CRUD in GraphQL mutations, keep domain logic in Apex.
Important enforcement warning
This is the part that teams get wrong during migration.
If your guardrail only exists in an Apex controller method, and your LWC now writes through GraphQL, that controller logic does not run.
So if a user has object/field access, they can still attempt the write through GraphQL unless the rule is enforced by the platform or data layer.
For non-negotiable rules, enforce them in layers that always execute on data change:
- validation rules
- before-save flows
- triggers/domain logic invoked by triggers
- object permissions, field-level security, sharing model
Client-side checks and controller-specific checks are convenience layers, not authoritative enforcement.
Guardrails that keep this production-safe
- Prefer GraphQL variables over string interpolation.
- Handle
errorsevery time, even when data is present. - Keep mutation payloads focused; avoid huge unrelated batches.
- Use aliases in multi-op requests for readability and reliable parsing.
- Validate object/field support early (UI API support + user access).
- Be explicit with
allOrNonefor transactional intent. - Refresh query-backed UI state after successful writes.
- Never rely on LWC-only or controller-only checks for critical validation.
Current limitations worth knowing
- Mutations apply to UI API-supported objects.
- Child record creation as nested payload is not supported in one single operation.
- Parent/child can still be created in one request as separate operations with references.
- Dependent operations are dependency-bound: if upstream dependency fails, downstream dependent fails.
lightning/graphqlmutation support is available in API66.0+.
FAQ
1) Do GraphQL mutations replace Apex in LWC?
No. They replace Apex for many basic CRUD flows, not for complex domain logic or integrations.
2) Are mutations secure?
They run through UI API behavior, so object access, field access, and sharing-aware data rules apply.
3) Should I migrate every existing Apex CRUD endpoint?
Not automatically. Start with low-risk, UI-centric CRUD screens where business logic is minimal.
4) Can existing Apex-based LWCs keep running?
Yes. Mutation support is additive. You can migrate incrementally per component.
5) What is the main benefit in one sentence?
You get a cleaner client-driven read/write model in LWC with less boilerplate backend code for standard record operations.
Final take
Spring '26 mutation support is less about replacing Apex and more about using Apex where it actually adds value.
For standard record workflows, GraphQL mutations give LWC teams a faster, cleaner path from user action to committed data.