afv-library/skills/integration-connectivity-generate/references/rest-callout-patterns.md

8.8 KiB

REST Callout Patterns

Overview

This guide covers patterns for making HTTP callouts from Salesforce Apex to external REST APIs.

Synchronous vs Asynchronous

When to Use Synchronous

  • User needs immediate response
  • Called from Visualforce, LWC, or Aura
  • NOT triggered by DML operations
  • Response required before next action

When to Use Asynchronous

  • Called from triggers (REQUIRED)
  • Fire-and-forget operations
  • Background processing
  • Long-running operations
┌─────────────────────────────────────────────────────────────────┐
│  CALLOUT CONTEXT DECISION                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Is this called from a trigger or after DML?                    │
│  ├── YES → Use Queueable with Database.AllowsCallouts          │
│  └── NO  → Synchronous OK                                       │
│                                                                 │
│  Does user need immediate response?                             │
│  ├── YES → Synchronous (if allowed)                            │
│  └── NO  → Consider async for better UX                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Basic Request Pattern

public class RestCallout {

    public static HttpResponse makeRequest(String method, String endpoint, String body) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:MyCredential' + endpoint);
        req.setMethod(method);
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(120000); // 120 seconds max

        if (String.isNotBlank(body)) {
            req.setBody(body);
        }

        return new Http().send(req);
    }
}

HTTP Methods

Method Use Case Body
GET Retrieve resource No
POST Create resource Yes
PUT Full update Yes
PATCH Partial update Yes
DELETE Remove resource Usually No

Response Handling

Status Code Categories

Integer statusCode = response.getStatusCode();

if (statusCode >= 200 && statusCode < 300) {
    // Success (2xx)
} else if (statusCode >= 400 && statusCode < 500) {
    // Client error (4xx) - don't retry
} else if (statusCode >= 500) {
    // Server error (5xx) - may retry
}

Common Status Codes

Code Meaning Action
200 OK Process response
201 Created Resource created
204 No Content Success, no body
400 Bad Request Fix request
401 Unauthorized Check credentials
403 Forbidden Check permissions
404 Not Found Resource doesn't exist
429 Too Many Requests Rate limited, retry later
500 Server Error Retry with backoff
503 Service Unavailable Retry later

Error Handling Pattern

public class ApiClient {

    public static Map<String, Object> callApi(String endpoint) {
        try {
            HttpResponse res = makeRequest('GET', endpoint, null);

            if (res.getStatusCode() == 200) {
                return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            }

            // Handle specific errors
            if (res.getStatusCode() == 404) {
                throw new NotFoundException('Resource not found: ' + endpoint);
            }

            if (res.getStatusCode() == 429) {
                String retryAfter = res.getHeader('Retry-After');
                throw new RateLimitedException('Rate limited. Retry after: ' + retryAfter);
            }

            throw new ApiException('API Error: ' + res.getStatusCode() + ' - ' + res.getBody());

        } catch (CalloutException e) {
            // Network error, timeout, SSL error
            throw new ApiException('Connection failed: ' + e.getMessage(), e);
        }
    }

    public class ApiException extends Exception {}
    public class NotFoundException extends Exception {}
    public class RateLimitedException extends Exception {}
}

Retry Pattern

public class RetryableCallout {

    private static final Integer MAX_RETRIES = 3;
    private static final Set<Integer> RETRYABLE_CODES = new Set<Integer>{
        408, 429, 500, 502, 503, 504
    };

    public static HttpResponse callWithRetry(HttpRequest request) {
        Integer attempts = 0;

        while (attempts < MAX_RETRIES) {
            HttpResponse res = new Http().send(request);

            if (!RETRYABLE_CODES.contains(res.getStatusCode())) {
                return res;
            }

            attempts++;
            System.debug('Retry ' + attempts + ' for ' + res.getStatusCode());
        }

        throw new CalloutException('Max retries exceeded');
    }
}

Queueable Pattern (Async)

public class AsyncCallout implements Queueable, Database.AllowsCallouts {

    private Id recordId;

    public AsyncCallout(Id recordId) {
        this.recordId = recordId;
    }

    public void execute(QueueableContext context) {
        // Query record
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :recordId];

        // Make callout
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:MyAPI/accounts');
        req.setMethod('POST');
        req.setBody(JSON.serialize(new Map<String, Object>{
            'name' => acc.Name,
            'sfId' => acc.Id
        }));

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

        // Update record with result
        if (res.getStatusCode() == 201) {
            acc.External_Id__c = extractId(res.getBody());
            update acc;
        }
    }

    private String extractId(String body) {
        Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(body);
        return (String) result.get('id');
    }
}

// Usage from trigger:
// System.enqueueJob(new AsyncCallout(accountId));

Pagination Pattern

public class PaginatedApiClient {

    public static List<Map<String, Object>> getAllRecords(String endpoint) {
        List<Map<String, Object>> allRecords = new List<Map<String, Object>>();
        String nextPageUrl = endpoint;

        while (String.isNotBlank(nextPageUrl)) {
            HttpResponse res = makeRequest('GET', nextPageUrl, null);
            Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());

            // Add records from this page
            List<Object> records = (List<Object>) response.get('data');
            for (Object rec : records) {
                allRecords.add((Map<String, Object>) rec);
            }

            // Get next page URL
            nextPageUrl = (String) response.get('nextPage');
        }

        return allRecords;
    }
}

Governor Limits

Limit Value
Callouts per transaction 100
Maximum timeout 120,000 ms (120 seconds)
Maximum request size 6 MB
Maximum response size 6 MB
Concurrent long-running requests 10

Testing Callouts

@isTest
private class ApiClientTest {

    @isTest
    static void testSuccessfulCallout() {
        // Set mock
        Test.setMock(HttpCalloutMock.class, new MockSuccess());

        Test.startTest();
        Map<String, Object> result = ApiClient.callApi('/endpoint');
        Test.stopTest();

        System.assertEquals('value', result.get('key'));
    }

    private class MockSuccess implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setBody('{"key": "value"}');
            return res;
        }
    }
}

Best Practices

  1. Always use Named Credentials - Never hardcode endpoints or credentials
  2. Set appropriate timeouts - Default may be too short for slow APIs
  3. Handle all error cases - Don't assume success
  4. Log requests and responses - Essential for debugging
  5. Use async for trigger contexts - Queueable with AllowsCallouts
  6. Implement retry logic - For transient failures
  7. Monitor governor limits - Especially callout count
  8. Parse errors gracefully - APIs return errors in various formats