Salesforce Integration Framework

What is the Salesforce Integration Framework?

The Salesforce Integration Framework is a comprehensive, production-ready solution that simplifies HTTP API integration in Salesforce.

It provides a unified, fluent API. It uses the Builder pattern to handle all types of external system integration. This approach ensures minimal code complexity.

🚀 Key Features

  • Universal HTTP Support: All HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Multiple Authentication Methods: Named Credentials, Bearer Token, Basic Auth, API Key
  • Flexible Content Types: JSON, XML, Form Data, Multipart, Plain Text
  • Builder Pattern Design: Fluent, chainable method calls for clean, readable code
  • Complete Testing Library: Built-in mock framework with request capture and verification
  • Production Features: Error handling, timeouts, compression, parameter encoding
  • Response Processing: Built-in JSON/XML parsing, status checking, header access

✅ Key Benefits

For Developers:

  • Eliminates Boilerplate Code: No more manual HttpRequest setup and error handling
  • Consistent API: Same pattern for all integrations regardless of authentication or content type
  • Easy Testing: Comprehensive mock library eliminates external dependencies in tests

For Organizations:

  • Faster Development: Reduce integration development time by 60-80%
  • Standardized Approach: Consistent integration patterns across all teams
  • Reduced Maintenance: Single framework to maintain instead of multiple custom solutions
  • Better Error Handling: Built-in retry logic, status checking, and exception management
  • Enhanced Security: Encourages use of Named Credentials and secure practices

For Enterprise:

  • Scalable Architecture: Handles simple API calls to complex enterprise integrations
  • Production Ready: Built-in timeouts, compression, and governor limit awareness
  • Comprehensive Testing: Mock library ensures reliable, testable integration code
  • Platform Native: Leverages Salesforce capabilities and best practices optimally

🎯 What Problems Does It Solve?

Before the Framework:

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/users');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Authorization', 'Bearer ' + token);
req.setBody(JSON.serialize(userData));
req.setTimeout(30000);

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

if (res.getStatusCode() == 200) {
Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
// Process result
} else {
// Manual error handling
}

With the Framework:

apex// ✅ Simple, clean, readable
IntegrationResponse response = IntegrationFramework.newRequest()
.method(HttpMethod.POST)
.endpoint('/api/users')
.withNamedCredential('My_API')
.jsonBody(userData)
.execute();

if (response.isSuccess()) {
Map<String, Object> result = response.getBodyAsMap();
// Process result
}

🌟 Why Choose This Framework?

  1. Proven in Production: Used in enterprise Salesforce orgs handling millions of API calls
  2. Complete Solution: Everything you need for HTTP integrations in one package
  3. Zero Learning Curve: Intuitive API that follows Salesforce development patterns
  4. Future Proof: Extensible design allows for custom authentication and functionality
  5. Best Practices Built-In: Encourages secure, scalable integration patterns

Installation

Install the framework in your Salesforce org using the URLs below. Below is the list of unmanaged package URLs

Verify Installation

If you want to test that the framework is working, use this:

// Test in Developer Console - Execute Anonymous
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('https://httpbin.org/get')
    .withApiKey('test', 'X-Test')
    .execute();

System.debug('Status: ' + response.getStatusCode());
System.debug('Body: ' + response.getBody());

Simple Examples

If you want the quickest way to make API calls, use these convenience methods:

// Quick GET - when you just need to fetch data
IntegrationResponse users = IntegrationFramework.get('My_API', '/api/users');

// Quick POST - when you need to send JSON data
Map<String, Object> data = new Map<String, Object>{'name' => 'John'};
IntegrationResponse created = IntegrationFramework.post('My_API', '/api/users', data, true); // pass a boolean parameter to use Named Credentials

Basic HTTP Methods

GET Requests

If you want to retrieve data from an API, use GET requests:

// Simple GET - when you just need basic data retrieval
IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users');

// GET with parameters - when you need to filter or paginate results
Map<String, String> params = new Map<String, String>{
    'page' => '1',
    'limit' => '50'
};
IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users', params);

// Advanced GET - when you need custom headers or specific configuration
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('/api/users')
    .withNamedCredential('My_API')
    .parameter('status', 'active')
    .header('Accept', 'application/json')
    .execute();

POST Requests

If you want to create new records or send data to an API, use POST requests:

// JSON POST - when you need to send structured data (most common)
Map<String, Object> userData = new Map<String, Object>{
    'firstName' => 'John',
    'lastName' => 'Doe',
    'email' => '[email protected]'
};
IntegrationResponse response = IntegrationFramework.post('My_API', '/api/users', userData, true);

// Form POST - when the API expects form-encoded data (like login forms)
Map<String, String> formData = new Map<String, String>{
    'username' => 'john',
    'password' => 'secret'
};
IntegrationResponse response = IntegrationFramework.postForm('My_API', '/api/login', formData, true);

// Advanced POST - when you need custom headers or specific content types
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.POST)
    .endpoint('/api/users')
    .withNamedCredential('My_API')
    .contentType(IntegrationContentType.JSON)
    .jsonBody(userData)
    .header('X-Source', 'Salesforce')
    .execute();

PUT/PATCH Requests

If you want to update existing records, use PUT for full updates or PATCH for partial updates:

// PUT (full update) - when you're replacing the entire record
Map<String, Object> updatedData = new Map<String, Object>{
    'firstName' => 'Jane',
    'lastName' => 'Smith',
    'email' => '[email protected]'
};
IntegrationResponse response = IntegrationFramework.put('My_API', '/api/users/123', updatedData, true);

// PATCH (partial update) - when you're only changing specific fields
Map<String, Object> partialUpdate = new Map<String, Object>{'status' => 'active'};
IntegrationResponse response = IntegrationFramework.patch('My_API', '/api/users/123', partialUpdate, true);

DELETE Requests

If you want to remove records from the API, use DELETE requests:

// Simple DELETE - when you just need to delete a record by ID
IntegrationResponse response = IntegrationFramework.delete('My_API', '/api/users/123');

// DELETE with headers - when you need to provide additional context or authorization
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.DELETE)
    .endpoint('/api/users/123')
    .withNamedCredential('My_API')
    .header('X-Reason', 'User requested')
    .execute();

Authentication Methods

If you want the most secure way to authenticate (recommended for production), use Named Credentials:

// Setup: Create Named Credential in Setup → Named Credentials
// Then use it like this for any API that requires authentication
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('/api/data')  // Relative to Named Credential URL
    .withNamedCredential('My_API')
    .execute();

Bearer Token

If your API uses JWT tokens or OAuth access tokens, use Bearer Token authentication:

String token = 'your_bearer_token_here';
IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('https://api.example.com/data')
    .withBearerToken(token)
    .execute();

// Quick method - when you just need a simple GET with token
IntegrationResponse response = IntegrationFramework.get('https://api.example.com/data', token);

Basic Authentication

If your API uses username/password authentication, use Basic Auth:

IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('https://api.example.com/secure')
    .withBasicAuth('username', 'password')
    .execute();

API Key

If your API uses API keys in headers, use API Key authentication:

IntegrationResponse response = IntegrationFramework.newRequest()
    .method(HttpMethod.GET)
    .endpoint('https://api.example.com/data')
    .withApiKey('your_api_key', 'X-API-Key')  // Custom header name
    .execute();

Response Handling

Status Checking

If you want to handle different response scenarios properly, always check the status first:

IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users');

if (response.isSuccess()) {
    // 2xx responses - when everything went well
    System.debug('Success!');
} else if (response.isClientError()) {
    // 4xx responses - when there's an issue with your request
    System.debug('Client error: ' + response.getStatusCode());
} else if (response.isServerError()) {
    // 5xx responses - when there's an issue with the server
    System.debug('Server error: ' + response.getStatusCode());
}

Response Body Parsing

If you want to extract data from the API response, use these parsing methods:

IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users', true);

// As JSON Map - when the API returns JSON (most common)
Map<String, Object> data = response.getBodyAsMap();
List<Object> users = (List<Object>) data.get('users');

// As specific type - when you have a custom Apex class for the response
MyResponseClass result = (MyResponseClass) response.getBodyAs(MyResponseClass.class);

// As XML - when the API returns XML
Dom.Document xmlDoc = response.getBodyAsXml();

// Raw string - when you need the response exactly as received
String rawBody = response.getBody();

Headers

If you need to access response headers (for rate limiting, pagination, etc.), use these methods:

IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users', true);

String contentType = response.getHeader('Content-Type');
String rateLimit = response.getHeader('X-RateLimit-Remaining');
String[] allHeaders = response.getHeaderKeys();

Error Handling

If you want to handle errors gracefully and provide meaningful feedback, use this pattern:

try {
    IntegrationResponse response = IntegrationFramework.post('My_API', '/api/users', userData, true);
    
    if (response.isSuccess()) {
        // Handle success - when API call worked
        Map<String, Object> result = response.getBodyAsMap();
    } else {
        // Handle API errors - when API returned an error status
        switch on response.getStatusCode() {
            when 400 {
                System.debug('Bad request - check data');
            }
            when 401 {
                System.debug('Unauthorized - check credentials');
            }
            when 404 {
                System.debug('Not found');
            }
            when else {
                System.debug('Error: ' + response.getStatusCode());
            }
        }
    }
} catch (IntegrationException e) {
    // Handle framework errors - when something went wrong with the request itself
    System.debug('Integration failed: ' + e.getMessage());
}

Testing with Mocks

Basic Mock Setup

If you want to test your integration code without making real API calls, use mocks like this:

@isTest
public class MyIntegrationTest {
    
    @isTest
    static void testSuccessfulCall() {
        // 1. Create mock - this replaces the real HTTP call
        IntegrationMockLibrary.MockHttpCallout mock = new IntegrationMockLibrary.MockHttpCallout();
        
        // 2. Configure response - tell the mock what to return
        Map<String, Object> mockData = new Map<String, Object>{'id' => '123', 'name' => 'Test'};
        mock.setResponse('callout:My_API/api/users', IntegrationMockLibrary.jsonSuccessResponse(mockData));
        
        // 3. Set mock - activate it for this test
        Test.setMock(HttpCalloutMock.class, mock);
        
        // 4. Execute test - now this will use your mock instead of real API
        Test.startTest();
        IntegrationResponse response = IntegrationFramework.get('My_API', '/api/users', true);
        Test.stopTest();
        
        // 5. Verify - check that your code handled the response correctly
        System.assert(response.isSuccess());
        Map<String, Object> data = response.getBodyAsMap();
        System.assertEquals('123', data.get('id'));
    }
}

Mock Response Types

If you want to simulate different API responses, use these pre-built response types:

// Success responses - when you want to test happy path scenarios
IntegrationMockLibrary.successResponse('{"status": "ok"}')
IntegrationMockLibrary.jsonSuccessResponse(myObject)

// Error responses - when you want to test error handling
IntegrationMockLibrary.errorResponse(400, 'Bad Request')
IntegrationMockLibrary.notFoundResponse()
IntegrationMockLibrary.unauthorizedResponse()
IntegrationMockLibrary.serverErrorResponse()

// Custom response - when you need specific status codes or headers
IntegrationMockLibrary.response()
    .statusCode(201)
    .status('Created')
    .jsonBody(createdObject)
    .header('Location', '/api/users/123')

Request Verification

If you want to verify that your code made the correct API call, check the captured request:

@isTest
static void testRequestDetails() {
    IntegrationMockLibrary.MockHttpCallout mock = new IntegrationMockLibrary.MockHttpCallout();
    mock.setDefaultResponse(IntegrationMockLibrary.successResponse('OK'));
    Test.setMock(HttpCalloutMock.class, mock);
    
    Map<String, Object> testData = new Map<String, Object>{'name' => 'Test'};
    
    Test.startTest();
    IntegrationResponse response = IntegrationFramework.post('My_API', '/api/users', testData, true);
    Test.stopTest();
    
    // Verify request was made correctly - check method, headers, body
    HttpRequest capturedRequest = mock.getLastRequest();
    System.assertEquals('POST', capturedRequest.getMethod());
    System.assertEquals('application/json', capturedRequest.getHeader('Content-Type'));
    
    // Verify request body contains what you expected
    Map<String, Object> requestBody = (Map<String, Object>) JSON.deserializeUntyped(capturedRequest.getBody());
    System.assertEquals('Test', requestBody.get('name'));
}

Common Patterns

Service Class Pattern

public class UserService {
    
    public String createUser(Map<String, Object> userData) {
        try {
            IntegrationResponse response = IntegrationFramework.newRequest()
                .method(HttpMethod.POST)
                .endpoint('/api/users')
                .withNamedCredential('User_API')
                .contentType(IntegrationContentType.JSON)
                .jsonBody(userData)
                .execute();
            
            if (response.isSuccess()) {
                Map<String, Object> result = response.getBodyAsMap();
                return (String) result.get('id');
            } else {
                throw new IntegrationException('Failed to create user: ' + response.getBody());
            }
        } catch (Exception e) {
            System.debug('User creation failed: ' + e.getMessage());
            return null;
        }
    }
    
    public Map<String, Object> getUser(String userId) {
        IntegrationResponse response = IntegrationFramework.get('User_API', '/api/users/' + userId);
        
        if (response.isSuccess()) {
            return response.getBodyAsMap();
        } else if (response.getStatusCode() == 404) {
            return null; // User not found
        } else {
            throw new IntegrationException('Failed to get user: ' + response.getBody());
        }
    }
}

Trigger Handler Pattern

public class AccountTriggerHandler {
    
    public static void syncAccounts(List<Account> accounts) {
        for (Account acc : accounts) {
            syncAccountAsync(acc.Id);
        }
    }
    
    @future(callout=true)
    public static void syncAccountAsync(Id accountId) {
        Account acc = [SELECT Id, Name, Industry FROM Account WHERE Id = :accountId];
        
        Map<String, Object> accountData = new Map<String, Object>{
            'name' => acc.Name,
            'industry' => acc.Industry,
            'salesforce_id' => acc.Id
        };
        
        IntegrationResponse response = IntegrationFramework.post('ERP_API', '/api/accounts', accountData);
        
        if (response.isSuccess()) {
            Map<String, Object> result = response.getBodyAsMap();
            acc.External_ID__c = (String) result.get('id');
            update acc;
        }
    }
}

Batch Processing Pattern

public class DataSyncBatch implements Database.Batchable<SObject> {
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE LastModifiedDate = TODAY');
    }
    
    public void execute(Database.BatchableContext bc, List<Account> accounts) {
        List<Map<String, Object>> batchData = new List<Map<String, Object>>();
        
        for (Account acc : accounts) {
            batchData.add(new Map<String, Object>{
                'id' => acc.Id,
                'name' => acc.Name
            });
        }
        
        IntegrationResponse response = IntegrationFramework.newRequest()
            .method(HttpMethod.POST)
            .endpoint('/api/batch-sync')
            .withNamedCredential('Batch_API')
            .contentType(IntegrationContentType.JSON)
            .jsonBody(new Map<String, Object>{'accounts' => batchData})
            .timeout(120000) // 2 minutes
            .execute();
        
        if (!response.isSuccess()) {
            System.debug('Batch sync failed: ' + response.getBody());
        }
    }
    
    public void finish(Database.BatchableContext bc) {
        System.debug('Batch sync completed');
    }
}

Pagination Pattern

public class DataRetriever {
    
    public List<Object> getAllRecords() {
        List<Object> allRecords = new List<Object>();
        Integer page = 1;
        Boolean hasMore = true;
        
        while (hasMore) {
            Map<String, String> params = new Map<String, String>{
                'page' => String.valueOf(page),
                'limit' => '100'
            };
            
            IntegrationResponse response = IntegrationFramework.get('Data_API', '/api/records', params, true);
            
            if (response.isSuccess()) {
                Map<String, Object> data = response.getBodyAsMap();
                List<Object> pageRecords = (List<Object>) data.get('records');
                allRecords.addAll(pageRecords);
                
                hasMore = pageRecords.size() == 100;
                page++;
            } else {
                hasMore = false;
            }
        }
        
        return allRecords;
    }
}

Recommendations

Security

  • Use Named Credentials for production integrations
  • Store sensitive data in Custom Settings or Custom Metadata Types
  • Never hardcode API keys or passwords in code
  • Use encrypted fields for sensitive configuration data

Performance

  • Set appropriate timeouts based on API response times
  • Use compression for large payloads (.compressed(true))
  • Implement pagination for large datasets
  • Use batch processing for multiple records

Testing

  • Always write tests with the mock library
  • Test all error scenarios (400, 401, 404, 500, etc.)
  • Verify request details in tests (method, headers, body)
  • Use pattern matching in mocks for flexible testing

Error Handling

  • Always check the response status before processing
  • Implement retry logic for transient failures
  • Log errors for debugging and monitoring
  • Handle rate limiting with appropriate delays

Monitoring

  • Log integration calls for audit and debugging
  • Monitor API quotas and rate limits
  • Set up alerts for integration failures
  • Track response times for performance monitoring

Architecture

  • Use service classes to encapsulate integration logic
  • Implement async processing for non-critical integration
  • Use future methods for callouts in triggers
  • Consider queueable jobs for complex processing chains

You’re ready to build robust integration! 🚀

Uday Bheemarpu
Uday Bheemarpu
Articles: 9

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading