Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
DictionaryAApex Controller
DevelopmentAdvanced

Apex Controller

An Apex Controller is a server-side Apex class that backs a Salesforce UI page, exposing methods that the front-end can call to load and persist data.

§ 01

Definition

An Apex Controller is a server-side Apex class that backs a Salesforce UI page, exposing methods that the front-end can call to load and persist data. The role of a controller is to bridge the UI runtime (Visualforce, Aura, or Lightning Web Components) and the Salesforce data model. Controllers are where the SOQL queries, DML operations, business logic, and platform integrations live, while the UI layer focuses on rendering and user interaction.

The term means different things across the three UI frameworks. In Visualforce, a controller is a class declared on the page tag and tightly bound to the page lifecycle. In Aura, a server-side controller is a class with @AuraEnabled methods called from the client controller. In LWC, the same concept appears as an Apex class with @AuraEnabled methods wired through @wire or imperative calls. Across all three, the controller pattern is the same: a thin server-side class that exposes methods, enforces security, and returns data.

§ 02

How Apex Controllers actually run in a Salesforce org

Three flavors: Visualforce, Aura, LWC

The Apex Controller has three distinct shapes depending on which UI framework consumes it. Visualforce controllers can be standard controllers (handed to you by the platform for a given sObject), extensions (your custom class layered on top of a standard controller), or custom controllers (entirely your code, declared via the controller attribute on apex:page). Aura and LWC controllers do not pair with a single page. They are stateless Apex classes with @AuraEnabled methods, callable from any client component. The shift from Visualforce to Lightning collapsed the controller from a stateful object to a pure RPC endpoint.

The @AuraEnabled annotation

@AuraEnabled is what makes an Apex method callable from a Lightning client. Without it, the method is invisible to Aura or LWC. The cacheable=true variant tells the framework the method has no side effects, so the Lightning Data Service can cache the response and reuse it across components. Mutations (insert, update, delete) must use plain @AuraEnabled without cacheable, because cached writes break the data contract. Many performance issues trace back to missing cacheable on a read method.

Security enforcement: with sharing, without sharing, inherited sharing

An Apex Controller runs in the system context by default unless declared otherwise. with sharing makes the class respect the running user's sharing rules. without sharing ignores sharing and runs in full system access. inherited sharing takes the mode from the calling class. Forgetting to declare with sharing on a controller that reads sensitive data is one of the most common security review findings on AppExchange submissions. Default to with sharing and only opt out when you specifically need elevated access.

Governor limits inside controller methods

Every Apex Controller method runs inside an Apex transaction with governor limits: 100 SOQL queries, 150 DML statements, 50,000 rows queried, 10,000 ms of CPU time, 6 MB of heap. Methods that aggregate data across many records hit these limits more often than methods that operate on a single record. The fix is usually bulkification: read or write in batches, use SOQL for-loops to stream, and avoid nested loops with queries inside. Profile slow controllers in the Developer Console replay viewer before reaching for batch Apex.

Returning data: AuraEnabled return types

@AuraEnabled methods can return primitives (String, Integer, Boolean), sObjects (Account, Case, custom objects), Lists or Maps of those, or custom Apex classes you define. Custom classes must have @AuraEnabled-annotated properties to be serializable to the client. Returning a Map<sObject, Decimal> is supported but verbose to consume in JavaScript. Returning a wrapper class with named properties is usually cleaner. Wrappers also let you decorate the response with computed fields that do not exist on the database.

Error handling and AuraHandledException

When an Apex Controller method throws an exception, the framework serializes it and sends a message to the client. The default message exposes internal stack traces, which is both a security concern and a usability concern. Wrap risky code in a try/catch, log the real exception, and rethrow a new AuraHandledException with a clean, user-facing message. The client receives the AuraHandledException's getMessage() and never sees the stack. Most Lightning UI errors readers complain about (System.NullPointerException visible on screen) are missing AuraHandledException wrappers.

Testing controllers

Controllers are tested like any other Apex class with @IsTest methods. The Test.setMock pattern injects fake responses for HTTP callouts. The Test.startTest / Test.stopTest envelope flushes governor counts and triggers async execution. AuraEnabled methods are called directly from the test class, no client simulation needed. The platform requires 75 percent code coverage to deploy to production, but coverage alone is not quality. A test that calls a method and ignores the result counts toward coverage and contributes nothing to confidence. Assert the return values, the DML effects, and the exception paths.

§ 03

Writing an Apex Controller for a Lightning component

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.

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

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

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

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

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

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

Key options
@AuraEnabledremember

Annotation that marks an Apex method as callable from Lightning components. Without it, the method is invisible to the client.

cacheable=trueremember

Annotation modifier that flags a method as a pure read. The Lightning Data Service caches the response and reuses it across components.

with sharingremember

Class-level modifier that enforces the running user's sharing rules. The recommended default for any controller that handles user-scoped data.

without sharingremember

Class-level modifier that runs the class in full system access, ignoring sharing. Use intentionally and document the reason.

inherited sharingremember

Class-level modifier that takes the sharing context from the calling class. Useful for helper classes invoked from controllers in both modes.

AuraHandledExceptionremember

Exception type that returns a clean, user-facing message to the Lightning client without exposing the stack trace.

Gotchas
  • 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.
§

Trust & references

Sources

Cross-checked against the following references.

Official documentation

Straight from the source - Salesforce's reference material on Apex Controller.

Keep learning

Hands-on resources to go deeper on Apex Controller.

Was this entry helpful?
Help us write better definitions. Quick reactions or detailed edit suggestions.

About the Author

Dipojjal Chakrabarti is a B2C Solution Architect with 29 Salesforce certifications and over 13 years in the Salesforce ecosystem. He runs salesforcedictionary.com to help admins, developers, architects, and cert/interview candidates sharpen their fundamentals. More about Dipojjal.

§

Test your knowledge

Q1. What is an Apex Controller?

Q2. Which of these does Salesforce provide automatically for every sObject?

Q3. What is a Controller Extension?

§

Discussion

Loading…

Loading discussion…