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
RestEntryPointclass intercepts all requests (GET,POST,PUT,PATCH,DELETE) and leverages theEndpointDefinitionMatcherclass to resolve incoming request URIs - The
Endpoint_Definition__mdtCustom Metadata Type is used to register endpoint configurations. The configuration defines the HTTP method, endpoint pattern, handler class name, active state, etc. - The
Requestableinterface 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
HttpContextclass 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
@RestResourceclasses 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
Requestableenforces consistency across REST API integrations
