Skip to content

REST

The REST submodule provides a centralized, routing-based framework for handling inbound requests via custom REST APIs (i.e., Apex REST). Instead of writing boilerplate code for each Apex REST class, a single entry point dynamically maps and delegates incoming traffic.

Documentation

💾 Source Code

Implementation

  • The global RestEntryPoint class intercepts all requests (GET, POST, PUT, PATCH, DELETE) and leverages the EndpointDefinitionMatcher class to resolve incoming request URIs
  • The Endpoint_Definition__mdt Custom Metadata Type is used to register endpoint configurations. The configuration defines the HTTP method, endpoint pattern, handler class name, active state, etc.
  • The Requestable interface is implemented by endpoint handler classes and defines standard methods to handle incoming requests for all five supported HTTP verbs
  • Supports dynamic routing via path variable syntax (e.g., /accounts/{id}/contacts/{contactId}), matching on segment size and automatically extracting path values into a map of path variables passed to handlers
  • The HttpContext class provides status code mappings and constants for HTTP verbs and responses.

Demos

The below code demonstrates a simple implementation of the Requestable interface to handle various HTTP requests.

apex
/**
 * Sample implementation of the Requestable interface demonstrating
 * how to handle REST requests through the RestEntryPoint framework.
 */
public inherited sharing class AccountsRequestHandlerDemo implements Requestable {
    private final static String DEFAULT_CONTENT_TYPE = 'application/json';
    private final AccountsServiceDemo accountsService {get; set;}
    
    public AccountsRequestHandlerDemo() {
        this.accountsService = new AccountsServiceDemo(10);
    }

    // Handle HTTP POST (Create)
    public void handlePost(RestRequest request, RestResponse response, Endpoint_Definition__mdt endpointDefinition, Map<String, String> pathVariables) {
        AccountsServiceDemo.AccountDTO newAccount = (AccountsServiceDemo.AccountDTO)JSON.deserialize(request.requestBody.toString(), AccountsServiceDemo.AccountDTO.class);
        response.responseBody = Blob.valueOf(String.valueOf(this.accountsService.createAccount(newAccount.accountName)));
        response.statusCode = 201;
        response.addHeader('Content-Type', DEFAULT_CONTENT_TYPE);
    }
    
    // Handle HTTP GET (Retrieve)
    public void handleGet(RestRequest request, RestResponse response, Endpoint_Definition__mdt endpointDefinition, Map<String, String> pathVariables) {
        String accountId = request.params.get('accountId');
        response.responseBody = Blob.valueOf(JSON.serialize(this.accountsService.getAccountById(Integer.valueOf(accountId))));
        response.statusCode = 200;
        response.addHeader('Content-Type', DEFAULT_CONTENT_TYPE);
    }
    
    // Handle HTTP PUT (Update)
    public void handlePut(RestRequest request, RestResponse response, Endpoint_Definition__mdt endpointDefinition, Map<String, String> pathVariables) {
        String accountId = pathVariables.get('id');
        AccountsServiceDemo.AccountDTO accountToUpdate = (AccountsServiceDemo.AccountDTO)JSON.deserialize(request.requestBody.toString(), AccountsServiceDemo.AccountDTO.class);
        this.accountsService.updateAccountById(Integer.valueOf(accountId), accountToUpdate.accountName);
        response.statusCode = 204;
    }
    
    // Handle HTTP PATCH (Partial Update)
    public void handlePatch(RestRequest request, RestResponse response, Endpoint_Definition__mdt endpointDefinition, Map<String, String> pathVariables) {
        String accountId = pathVariables.get('id');
        AccountsServiceDemo.AccountDTO accountToUpdate = (AccountsServiceDemo.AccountDTO)JSON.deserialize(request.requestBody.toString(), AccountsServiceDemo.AccountDTO.class);
        this.accountsService.updateAccountById(Integer.valueOf(accountId), accountToUpdate.accountName);
        response.statusCode = 204;
    }

    // Handle HTTP DELETE (Delete)
    public void handleDelete(RestRequest request, RestResponse response, Endpoint_Definition__mdt endpointDefinition, Map<String, String> pathVariables) {
        String accountId = request.params.get('accountId');
        this.accountsService.deleteAccountById(Integer.valueOf(accountId));
        response.statusCode = 204;
    }
}
apex
/**
 * Demo class for interacting with in-memory Account data.
 */
public inherited sharing class AccountsServiceDemo {
    /**
     * Properties
     */
    private final Map<Integer, AccountDTO> accountsList {get;set;}
    private Integer currentNumberOfAccounts {get;set;}

    /**
     * Default constructor with params
     *
     * @param numberOfAccountsToCreate The number of accounts to create
     */
    public AccountsServiceDemo(Integer numberOfAccountsToCreate) {
        if (numberOfAccountsToCreate == null || numberOfAccountsToCreate < 1) {
            throw new IllegalArgumentException('Missing number of accounts to created');
        }
        this.accountsList = this.initializeDemoAccounts(numberOfAccountsToCreate);
        this.currentNumberOfAccounts = numberOfAccountsToCreate;
    }

    /**
     * Simulates the creation of an account 
     * 
     * @param accountName The name of the account to create
     * @return The Id of the created account
     */
    public Integer createAccount(String accountName) {
        if (String.isBlank(accountName)) {
            throw new IllegalArgumentException('Account Name must be provided');
        }
        this.currentNumberOfAccounts++;
        this.accountsList.put(this.currentNumberOfAccounts, new AccountDTO(this.currentNumberOfAccounts, accountName));
        return this.currentNumberOfAccounts;
    }

    /**
     * Returns an account by Id
     * 
     * @param accountId The Id of the account to retrieve
     * @return The retrieved account
     */
    public AccountDTO getAccountById(Integer accountId) {
        AccountDTO accountRecord = this.accountsList.get(accountId);
        if (accountRecord == null) {
            throw new QueryException('Account cannot be found for Id ' + accountId);
        }
        return accountRecord;
    }

    /**
     * Simulates updating an account (full or partial)
     * 
     * @param accountId The Id of the account to update
     * @param accountName The new name of the account
     */
    public void updateAccountById(Integer accountId, String accountName) {
        if (accountId == null) {
            throw new IllegalArgumentException('Account Id must be provided');
        }

        if (String.isBlank(accountName)) {
            throw new IllegalArgumentException('Account Name must be provided');
        }

        AccountDTO accountToUpdate = this.accountsList.get(accountId);
        if (accountToUpdate == null) {
            throw new QueryException('Account to update cannot be found for Id ' + accountId);
        }

        this.accountsList.put(accountId, new AccountDTO(accountId, accountName));
    }

    /**
     * Simulates the deletion of an account
     * 
     * @param accountId The Id of the account to delete
     */
    public void deleteAccountById(Integer accountId) {
        Integer startingNumberOfAccounts = this.accountsList.size();
        Object deletedAccount = this.accountsList.remove(accountId);
        if (deletedAccount == null) {
            throw new QueryException('Account to delete cannot be found for Id ' + accountId);
        }
    }

    /**
     * Creates collection of in-memory account records
     */
    private Map<Integer, AccountDTO> initializeDemoAccounts(Integer numberOfAccounts) {
        Map<Integer, AccountDTO> newAccounts = new Map<Integer, AccountDTO>();
        for (Integer i = 0; i < numberOfAccounts; i++) {
            newAccounts.put(i, new AccountDTO(i, 'Account ' + i));
        }
        return newAccounts;
    }

    /**
     * Inner class to represent a Data Transfer
     * Object (i.e., wrapper) for accounts
     */
    public class AccountDTO {
        /**
         * Properties
         */
        @AuraEnabled public final Integer accountId {get; private set;}
        @AuraEnabled public final String accountName {get; private set;}

        /**
         * Constructor with args
         * 
         * @param accountId The Id of an account
         * @param accountName The name of an account
         */
        public AccountDTO(Integer accountId, String accountName) {
            this.accountId = accountId;
            this.accountName = accountName;
        }
    }
}

Benefits

  • Eliminates the need to create new @RestResource classes for every endpoint. All endpoints are configured declaratively in metadata and routed through a single entry point
  • Automatically normalizes URIs and extracts path variables (e.g., /users/{id}), saving handlers from manually parsing request paths
  • RestEntryPoint automatically wraps unhandled errors, JSON serialization issues, or metadata mismatches into standard JSON error response structures
  • Standardizing handlers under Requestable enforces consistency across REST API integrations

SlightWork is part of the Wynforce ecosystem