NexusCS

APEX

Salesforce
Quick reference for APEX - Salesforce's strongly typed, object-oriented programming language for the Force.com platform.
salesforce
apex
force.com
soql
dml

Getting started

Introduction

APEX is Salesforce's proprietary Java-like programming language for the Force.com platform. Strongly typed, object-oriented, executes server-side.

Basic Syntax

// Class declaration
public class HelloWorld {
    public static void sayHello() {
        System.debug('Hello World!');
    }
}

Quick Example

// Query and update records
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = 'Technology'];

for (Account acc : accounts) {
    acc.Rating = 'Hot';
}

update accounts;

Anonymous Execution

// Execute in Developer Console
Account acc = new Account(Name='Acme Corp');
insert acc;
System.debug('Created: ' + acc.Id);

Data Types

Primitives

Type Description Example
Integer 32-bit number Integer i = 42;
Long 64-bit number Long l = 123456789L;
Decimal Precise decimals Decimal d = 3.14159;
Double 64-bit floating Double d = 3.14;
String Text data String s = 'Hello';
Boolean True/false Boolean b = true;
Date Date only Date d = Date.today();
DateTime Date and time DateTime dt = DateTime.now();
Blob Binary data Blob b = Blob.valueOf('data');
ID Salesforce ID ID accountId = acc.Id;

Collections

// List (ordered, duplicates allowed)
List<String> names = new List<String>{'Alice', 'Bob'};
names.add('Charlie');
String first = names[0];

// Set (unordered, unique)
Set<Integer> numbers = new Set<Integer>{1, 2, 3};
numbers.add(4);
Boolean hasTwo = numbers.contains(2);

// Map (key-value pairs)
Map<String, Integer> ages = new Map<String, Integer>();
ages.put('Alice', 30);
Integer aliceAge = ages.get('Alice');

sObjects

// Standard object
Account acc = new Account();
acc.Name = 'Acme Corp';
acc.Industry = 'Technology';

// Custom object (API name ends with __c)
Custom_Object__c obj = new Custom_Object__c();
obj.Custom_Field__c = 'value';

// Generic sObject
sObject s = new Account();
s.put('Name', 'Generic Account');

SOQL Queries

Basic Queries

// Simple query
List<Account> accounts = [SELECT Id, Name FROM Account];

// With WHERE clause
List<Contact> contacts = [
    SELECT Id, FirstName, LastName, Email
    FROM Contact
    WHERE LastName = 'Smith'
];

// With LIMIT
Contact c = [
    SELECT Id, Email
    FROM Contact
    WHERE Id = :contactId
    LIMIT 1
];

Advanced Queries

// ORDER BY and LIMIT
List<Opportunity> opps = [
    SELECT Id, Name, Amount
    FROM Opportunity
    WHERE Amount > 10000
    ORDER BY Amount DESC
    LIMIT 10
];

// Relationship queries (parent to child)
List<Account> accounts = [
    SELECT Id, Name,
        (SELECT Id, FirstName, LastName FROM Contacts)
    FROM Account
];

// Child to parent
List<Contact> contacts = [
    SELECT Id, FirstName, Account.Name
    FROM Contact
];

Query Operators

// Comparison operators
[SELECT Id FROM Account WHERE AnnualRevenue > 1000000]
[SELECT Id FROM Account WHERE Industry != 'Technology']

// IN and NOT IN
[SELECT Id FROM Account WHERE Industry IN ('Tech', 'Finance')]
[SELECT Id FROM Contact WHERE LastName NOT IN :nameSet]

// LIKE (wildcards)
[SELECT Id FROM Account WHERE Name LIKE 'Acme%']
[SELECT Id FROM Contact WHERE Email LIKE '%@example.com']

// AND, OR
[SELECT Id FROM Account WHERE Industry = 'Tech' AND AnnualRevenue > 1000000]

// Date functions
[SELECT Id FROM Opportunity WHERE CloseDate = THIS_MONTH]
[SELECT Id FROM Case WHERE CreatedDate = LAST_N_DAYS:7]

Bind Variables

String industry = 'Technology';
Integer limit = 10;

List<Account> accounts = [
    SELECT Id, Name
    FROM Account
    WHERE Industry = :industry
    LIMIT :limit
];

DML Operations

Basic DML

// Insert
Account acc = new Account(Name='Acme Corp');
insert acc;

// Update
acc.Industry = 'Technology';
update acc;

// Upsert (insert or update based on ID/external ID)
upsert acc;

// Delete
delete acc;

// Undelete
undelete acc;

Bulk DML

// Insert multiple records
List<Account> accounts = new List<Account>{
    new Account(Name='Acme Corp'),
    new Account(Name='Global Inc'),
    new Account(Name='Tech Solutions')
};
insert accounts;

// Update multiple
for (Account acc : accounts) {
    acc.Industry = 'Technology';
}
update accounts;

// Delete multiple
delete accounts;

Database Methods

// Partial success allowed
Database.SaveResult[] results = Database.insert(accounts, false);

for (Database.SaveResult result : results) {
    if (!result.isSuccess()) {
        for (Database.Error error : result.getErrors()) {
            System.debug('Error: ' + error.getMessage());
        }
    }
}

// Update with partial success
Database.SaveResult[] updateResults = Database.update(accounts, false);

// Delete with partial success
Database.DeleteResult[] deleteResults = Database.delete(accounts, false);

// Upsert with external ID
Database.UpsertResult[] upsertResults = Database.upsert(accounts, Account.Fields.ExternalId__c, false);

Triggers

Trigger Structure

trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            // Before insert logic
        }
        if (Trigger.isUpdate) {
            // Before update logic
        }
    }

    if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            // After insert logic
        }
        if (Trigger.isUpdate) {
            // After update logic
        }
    }
}

Trigger Context Variables

Variable Type Description
Trigger.new List<sObject> New versions of records
Trigger.old List<sObject> Old versions (update/delete only)
Trigger.newMap Map<Id, sObject> Map of new records by Id
Trigger.oldMap Map<Id, sObject> Map of old records by Id
Trigger.isInsert Boolean True if insert operation
Trigger.isUpdate Boolean True if update operation
Trigger.isDelete Boolean True if delete operation
Trigger.isBefore Boolean True if before trigger
Trigger.isAfter Boolean True if after trigger
Trigger.isUndelete Boolean True if undelete operation
Trigger.size Integer Number of records

Trigger Example

trigger OpportunityTrigger on Opportunity (before insert, after update) {
    if (Trigger.isBefore && Trigger.isInsert) {
        // Set default values
        for (Opportunity opp : Trigger.new) {
            if (opp.Probability == null) {
                opp.Probability = 10;
            }
        }
    }

    if (Trigger.isAfter && Trigger.isUpdate) {
        // Update related records
        Set<Id> accountIds = new Set<Id>();
        for (Opportunity opp : Trigger.new) {
            if (opp.AccountId != null) {
                accountIds.add(opp.AccountId);
            }
        }

        // Process accounts
        List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
        // ... update logic
    }
}

Classes and Objects

Class Definition

public class Calculator {
    // Properties
    private Integer result;

    // Constructor
    public Calculator() {
        this.result = 0;
    }

    // Method
    public Integer add(Integer a, Integer b) {
        result = a + b;
        return result;
    }

    // Getter
    public Integer getResult() {
        return result;
    }
}

Access Modifiers

Modifier Description
private Only within class
public Within namespace
global All APEX code
protected Within class and subclasses

Static Methods

public class MathUtils {
    public static Integer square(Integer n) {
        return n * n;
    }

    public static Decimal average(List<Integer> numbers) {
        Integer sum = 0;
        for (Integer n : numbers) {
            sum += n;
        }
        return Decimal.valueOf(sum) / numbers.size();
    }
}

// Usage
Integer result = MathUtils.square(5); // 25

Inheritance

// Parent class
public virtual class Animal {
    public virtual void makeSound() {
        System.debug('Some sound');
    }
}

// Child class
public class Dog extends Animal {
    public override void makeSound() {
        System.debug('Bark!');
    }
}

// Usage
Dog myDog = new Dog();
myDog.makeSound(); // Bark!

Interfaces

// Interface definition
public interface Drawable {
    void draw();
}

// Implementation
public class Circle implements Drawable {
    public void draw() {
        System.debug('Drawing a circle');
    }
}

Control Flow

If-Else

if (amount > 10000) {
    discount = 0.10;
} else if (amount > 5000) {
    discount = 0.05;
} else {
    discount = 0;
}

Switch Statement

switch on status {
    when 'Open' {
        System.debug('Case is open');
    }
    when 'Closed', 'Resolved' {
        System.debug('Case is closed');
    }
    when else {
        System.debug('Unknown status');
    }
}

// With sObject type
switch on myObject {
    when Account acc {
        System.debug('Account: ' + acc.Name);
    }
    when Contact con {
        System.debug('Contact: ' + con.LastName);
    }
}

For Loops

// Traditional for loop
for (Integer i = 0; i < 10; i++) {
    System.debug(i);
}

// List iteration
List<Account> accounts = [SELECT Id, Name FROM Account];
for (Account acc : accounts) {
    System.debug(acc.Name);
}

// Set iteration
Set<String> names = new Set<String>{'Alice', 'Bob', 'Charlie'};
for (String name : names) {
    System.debug(name);
}

While Loops

// While loop
Integer i = 0;
while (i < 10) {
    System.debug(i);
    i++;
}

// Do-while loop
Integer j = 0;
do {
    System.debug(j);
    j++;
} while (j < 10);

Testing

Test Class Structure

@isTest
private class AccountTest {
    @testSetup
    static void setup() {
        // Create test data (runs once per test class)
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 10; i++) {
            accounts.add(new Account(Name='Test ' + i));
        }
        insert accounts;
    }

    @isTest
    static void testInsert() {
        Test.startTest();

        Account acc = new Account(Name='Test Account');
        insert acc;

        Test.stopTest();

        // Assertions
        Account result = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
        System.assertEquals('Test Account', result.Name);
        System.assertNotEquals(null, result.Id);
    }
}

Assertions

// Equality
System.assertEquals(expected, actual);
System.assertEquals(expected, actual, 'Error message');
System.assertNotEquals(unexpected, actual);

// Null checks
System.assert(condition);
System.assert(condition, 'Error message');

Test Data Creation

@isTest
static void testWithData() {
    // Create test data
    Account acc = new Account(Name='Test Account', Industry='Technology');
    insert acc;

    Contact con = new Contact(
        FirstName='John',
        LastName='Doe',
        AccountId=acc.Id
    );
    insert con;

    Test.startTest();
    // Your test logic here
    Test.stopTest();

    // Verify results
    Contact result = [SELECT Id, AccountId FROM Contact WHERE Id = :con.Id];
    System.assertEquals(acc.Id, result.AccountId);
}

Mock Callouts

@isTest
global class MockHttpResponse implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest req) {
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"status":"success"}');
        res.setStatusCode(200);
        return res;
    }
}

@isTest
static void testCallout() {
    Test.setMock(HttpCalloutMock.class, new MockHttpResponse());

    Test.startTest();
    // Code that makes HTTP callout
    Test.stopTest();
}

Annotations

Common Annotations

Annotation Usage Description
@isTest Test classes/methods Mark as test code
@testSetup Test methods Setup test data
@future Methods Async execution
@AuraEnabled Methods Expose to Lightning
@InvocableMethod Methods Flow/Process Builder
@ReadOnly Methods Increase query limits
@HttpGet Methods REST GET endpoint
@HttpPost Methods REST POST endpoint
@HttpPut Methods REST PUT endpoint
@HttpDelete Methods REST DELETE endpoint
@HttpPatch Methods REST PATCH endpoint

Future Methods

public class AsyncProcessor {
    @future
    public static void processRecords(Set<Id> recordIds) {
        List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
        // Process accounts
        update accounts;
    }

    @future(callout=true)
    public static void makeCallout(String endpoint) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');

        Http http = new Http();
        HttpResponse res = http.send(req);
    }
}

AuraEnabled

public class AccountController {
    @AuraEnabled
    public static List<Account> getAccounts() {
        return [SELECT Id, Name FROM Account LIMIT 10];
    }

    @AuraEnabled
    public static void updateAccount(Id accountId, String newName) {
        Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
        acc.Name = newName;
        update acc;
    }
}

REST API

@RestResource(urlMapping='/accounts/*')
global class AccountRestService {
    @HttpGet
    global static Account getAccount() {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/') + 1);
        return [SELECT Id, Name FROM Account WHERE Id = :accountId];
    }

    @HttpPost
    global static String createAccount(String name, String industry) {
        Account acc = new Account(Name=name, Industry=industry);
        insert acc;
        return acc.Id;
    }
}

Exception Handling

Try-Catch-Finally

try {
    Account acc = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1];
    acc.Name = 'Acme Corp';
    update acc;
} catch (DmlException e) {
    System.debug('DML Error: ' + e.getMessage());
} catch (QueryException e) {
    System.debug('Query Error: ' + e.getMessage());
} catch (Exception e) {
    System.debug('General Error: ' + e.getMessage());
} finally {
    System.debug('Cleanup code');
}

Common Exceptions

Exception Description
DmlException DML operation failed
QueryException SOQL query issue
ListException List index out of bounds
NullPointerException Null reference access
MathException Math operation error
TypeException Type conversion error

Custom Exceptions

public class CustomException extends Exception {}

public class AccountProcessor {
    public static void processAccount(Account acc) {
        if (acc.Name == null) {
            throw new CustomException('Account name is required');
        }
        // Process account
    }
}

// Usage
try {
    AccountProcessor.processAccount(acc);
} catch (CustomException e) {
    System.debug('Custom error: ' + e.getMessage());
}

Governor Limits

Common Limits

Limit Synchronous Asynchronous
SOQL queries 100 200
DML statements 150 150
Heap size 6 MB 12 MB
CPU time 10,000 ms 60,000 ms
SOSL queries 20 20
Records per DML 10,000 10,000
Callouts 100 100

Checking Limits

System.debug('SOQL Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML Statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('Heap Size: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
System.debug('CPU Time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());

Bulkification

// BAD - Query in loop
for (Account acc : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
}

// GOOD - Query once, use map
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
    accountIds.add(acc.Id);
}

Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccount.containsKey(con.AccountId)) {
        contactsByAccount.put(con.AccountId, new List<Contact>());
    }
    contactsByAccount.get(con.AccountId).add(con);
}

Best Practices

Bulkification

// Always handle collections
trigger AccountTrigger on Account (after insert) {
    List<Contact> contactsToInsert = new List<Contact>();

    for (Account acc : Trigger.new) {
        contactsToInsert.add(new Contact(
            FirstName='Default',
            LastName='Contact',
            AccountId=acc.Id
        ));
    }

    if (!contactsToInsert.isEmpty()) {
        insert contactsToInsert;
    }
}

Query Optimization

// Select only needed fields
List<Account> accounts = [SELECT Id, Name FROM Account];

// Use WHERE with indexed fields
List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :accountIds];

// Use LIMIT to reduce records
Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];

// Use FOR loops with queries for large datasets
for (List<Account> accounts : [SELECT Id, Name FROM Account]) {
    // Process batch of 200 records
}

Trigger Framework

// One trigger per object
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    AccountTriggerHandler.handle();
}

// Handler class
public class AccountTriggerHandler {
    public static void handle() {
        if (Trigger.isBefore) {
            if (Trigger.isInsert) {
                beforeInsert(Trigger.new);
            }
            if (Trigger.isUpdate) {
                beforeUpdate(Trigger.new, Trigger.oldMap);
            }
        }
    }

    private static void beforeInsert(List<Account> newAccounts) {
        // Logic here
    }

    private static void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
        // Logic here
    }
}

Security

// Use WITH SECURITY_ENFORCED
List<Account> accounts = [
    SELECT Id, Name
    FROM Account
    WITH SECURITY_ENFORCED
];

// Check field accessibility
if (Schema.sObjectType.Account.fields.Name.isAccessible()) {
    // Query field
}

// Check CRUD permissions
if (Schema.sObjectType.Account.isCreateable()) {
    insert acc;
}

// Strip inaccessible fields
List<Account> accounts = [SELECT Id, Name, Industry FROM Account];
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, accounts);
accounts = decision.getRecords();

Batch APEX

Batch Class

global class AccountBatch implements Database.Batchable<sObject> {
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([
            SELECT Id, Name, Industry
            FROM Account
            WHERE Industry = null
        ]);
    }

    global void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account acc : scope) {
            acc.Industry = 'Other';
        }
        update scope;
    }

    global void finish(Database.BatchableContext bc) {
        System.debug('Batch job completed');
    }
}

// Execute batch
AccountBatch batch = new AccountBatch();
Database.executeBatch(batch, 200); // Batch size 200

Schedulable

global class ScheduledBatch implements Schedulable {
    global void execute(SchedulableContext sc) {
        AccountBatch batch = new AccountBatch();
        Database.executeBatch(batch);
    }
}

// Schedule job (run daily at 2 AM)
ScheduledBatch schedulable = new ScheduledBatch();
String cronExp = '0 0 2 * * ?';
System.schedule('Daily Account Batch', cronExp, schedulable);

Queueable APEX

Queueable Class

public class AccountQueueable implements Queueable {
    private List<Account> accounts;

    public AccountQueueable(List<Account> accounts) {
        this.accounts = accounts;
    }

    public void execute(QueueableContext context) {
        for (Account acc : accounts) {
            acc.Industry = 'Technology';
        }
        update accounts;
    }
}

// Enqueue job
List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
AccountQueueable queueable = new AccountQueueable(accounts);
System.enqueueJob(queueable);

Chaining Queueable Jobs

public class ChainedQueueable implements Queueable {
    public void execute(QueueableContext context) {
        // First job logic
        System.debug('First job executed');

        // Chain next job
        System.enqueueJob(new SecondQueueable());
    }
}

public class SecondQueueable implements Queueable {
    public void execute(QueueableContext context) {
        System.debug('Second job executed');
    }
}

Gotchas

Common Pitfalls

// ❌ Query/DML in loops
for (Account acc : accounts) {
    update acc; // Governor limit violation
}

// ✅ Bulkify
update accounts;

// ❌ Modifying Trigger.new in after triggers
if (Trigger.isAfter) {
    for (Account acc : Trigger.new) {
        acc.Name = 'Changed'; // Error!
    }
}

// ✅ Only modify in before triggers
if (Trigger.isBefore) {
    for (Account acc : Trigger.new) {
        acc.Name = 'Changed'; // OK
    }
}

// ❌ SOQL injection
String name = 'Acme';
List<Account> accounts = Database.query('SELECT Id FROM Account WHERE Name = \'' + name + '\'');

// ✅ Use bind variables
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :name];

Null Safety

// Always check for null
if (acc != null && acc.Name != null) {
    System.debug(acc.Name);
}

// Use safe navigation (not supported - use null checks)
String name = (acc != null) ? acc.Name : 'Unknown';

// Handle empty lists
List<Account> accounts = [SELECT Id FROM Account WHERE Name = 'NonExistent'];
if (!accounts.isEmpty()) {
    Account acc = accounts[0];
}

Also see