Writing an Apex Controller for a Lightning component means creating a server-side class with @AuraEnabled methods that the client calls through @wire or imperative invocations. The class lives in the same DX project as the component bundle and deploys alongside it.
- Create the Apex class
From the Developer Console: File, New, Apex Class. From the CLI: sf apex generate class --name MyController. Place it in force-app/main/default/classes alongside the LWC bundle that calls it.
- Declare the sharing context
Open the class and add public with sharing class MyController. with sharing should be the default. Only switch to without sharing for cases where you intentionally need elevated access, and document the reason in a class header comment.
- Write the AuraEnabled method
Add a method with the @AuraEnabled annotation. For read-only methods, add (cacheable=true). For methods that mutate data, leave it as plain @AuraEnabled. The method signature is what the client imports by name from the @salesforce/apex/ClassName.method module path.
- Enforce object and field permissions
Inside the method, check sObject access with Schema.sObjectType.MyObject.isAccessible() and field access with Schema.sObjectType.MyObject.fields.MyField.isAccessible(). The platform does not enforce field-level security automatically unless you use WITH SECURITY_ENFORCED in SOQL or Stripe.stripInaccessible.
- Wrap errors in AuraHandledException
Wrap the method body in try/catch. On exception, log the original message and throw a new AuraHandledException with a user-facing message. The client will receive only the friendly message, not the stack trace.
- Write the unit test
Create a test class with @IsTest, instantiate sample data with TestSetup, call the controller method, and assert the return value and any DML side effects. Aim for both happy-path and exception-path coverage.
Annotation that marks an Apex method as callable from Lightning components. Without it, the method is invisible to the client.
Annotation modifier that flags a method as a pure read. The Lightning Data Service caches the response and reuses it across components.
Class-level modifier that enforces the running user's sharing rules. The recommended default for any controller that handles user-scoped data.
Class-level modifier that runs the class in full system access, ignoring sharing. Use intentionally and document the reason.
Class-level modifier that takes the sharing context from the calling class. Useful for helper classes invoked from controllers in both modes.
Exception type that returns a clean, user-facing message to the Lightning client without exposing the stack trace.
- Without with sharing, a controller runs in system mode and can return records the user should not see. Default to with sharing and opt out only when needed.
- cacheable=true methods cannot perform DML or invoke any operation with side effects. The Lightning Data Service will reject the call at runtime.
- Returning sObjects directly serializes every accessible field, including sensitive ones. Use SELECT field-list SOQL and wrapper classes to control the response shape.
- Throwing System.NullPointerException or other runtime exceptions exposes the stack trace to the client. Always wrap and rethrow as AuraHandledException.
- Aura @AuraEnabled and LWC @AuraEnabled use the same annotation but different invocation paths. The class itself does not need to know which framework calls it, but the cache behavior differs.