Pre-release: sf-bedrock is under active development. APIs and docs may change before general availability.
Bedrock Docs
Repository
Frameworks

REST

REST gives Bedrock one Apex REST gateway at /services/apexrest/api and routes versioned endpoint roots from Rest_Config__mdt into Apex classes that extend Rest.

Overview

Rest is a small routing layer over Apex REST. A single gateway listens at /services/apexrest/api/*, reads Rest_Config__mdt, builds a Rest.Request, and calls the matching Rest subclass.

Use Rest when you want versioned API endpoints whose root mappings live in metadata, while endpoint behavior stays in focused Apex classes.

Reach for a direct @RestResource class instead when an endpoint is a one-off integration with no shared routing, versioning, settings, or sub-endpoint needs.

Quickstart

Create an endpoint class that extends Rest and override the HTTP methods it supports.

public inherited sharing class AccountRest extends Rest {
    public override void get(Rest.Request request) {
        respond(new Map<String, Object>{ 'accountId' => request.primaryId });
    }
}

Create one Rest_Config__mdt record:

FieldValue
Active__ctrue
Route__caccounts
Version__c1
Apex__cAccountRest

Then call:

/services/apexrest/api/v1/accounts/001000000000001AAA

Examples

Add a sub-endpoint

Sub-endpoints are inner classes that also extend Rest.

public inherited sharing class AccountRest extends Rest {
    public class Contacts extends Rest {
        public override void get(Rest.Request request) {
            respond(
                new Map<String, Object>{
                    'accountId' => request.primaryId,
                    'contactId' => request.secondaryId
                }
            );
        }
    }
}

That class handles:

/services/apexrest/api/v1/accounts/001000000000001AAA/contacts/003000000000001AAA

Read query params, body, and bulk ids

params is only the query string. body is only the JSON object body. ids is a convenience set populated from ?ids=... or body ids.

public override void post(Rest.Request request) {
    Set<Id> ids = request.ids;
    String include = request.params.get('include');
    Object records = request.body.get('records');

    respond(202, new Map<String, Object> {
        'accepted' => ids.size(),
        'include' => include,
        'hasRecords' => records != null
    });
}

Gate access in Apex

Override canAccess for endpoint-owned permission rules.

public override Boolean canAccess(Rest.Request request) {
    return FeatureFlag.isEnabled('api.accounts.enabled');
}

Routing

The gateway URL shape is:

/services/apexrest/api[/vN]/{root}[/{segment1}[/{segment2}[/{segment3}]]]

Rest_Config__mdt.Route__c owns the root segment. The version segment is optional; when it is absent, Rest uses the highest active version for that route.

After the root, these shapes are supported:

ShapeMeaning
/accountsroot endpoint
/accounts/{primaryContext}root endpoint with context
/accounts/contactsContacts sub-endpoint
/accounts/{primaryContext}/contactscontext plus sub-endpoint
/accounts/contacts/{primaryContext}sub-endpoint plus context
/accounts/{primaryContext}/contacts/{secondaryContext}context, sub-endpoint, secondary context

Sub-endpoint resolution wins over context. If contacts resolves to an inner Contacts class, it is treated as a sub-endpoint, not a context value.

Settings

Rest_Settings__c is a hierarchy custom setting. Blank or invalid values fall back to safe defaults.

FieldDefault behavior
Unknown_Route_Status_Code__c404
Inactive_Route_Status_Code__c404
Unsupported_Method_Status_Code__c405
Access_Denied_Status_Code__c403
Expose_Error_Details__cfalse

Testing

Use RestMock to seed route metadata and settings in memory.

@istest static void testAccountRoute_returnsAccountResponse() {
    Rest.setMock(new RestMock().seedConfigs(new List<Rest_Config__mdt> {
        new Rest_Config__mdt(
            Active__c = true,
            Route__c = 'accounts',
            Version__c = 1,
            Apex__c = 'AccountRest'
        )
    }));

    RestRequest request = new RestRequest();
    request.httpMethod = 'GET';
    request.requestURI = '/services/apexrest/api/v1/accounts';
    RestContext.request = request;
    RestContext.response = new RestResponse();

    RestGateway.doGet();

    Assert.areEqual(200, RestContext.response.statusCode, 'Expected the account route to resolve.');
}

How It Works

Three ideas explain everything Rest does.

One: metadata resolves only the root. Rest_Config__mdt chooses the first endpoint class by route and version. Endpoint classes own their sub-endpoints.

Two: the request is positional. The router exposes generic primaryContext, secondaryContext, primaryId, and secondaryId values. Endpoint classes can wrap those with semantic helper methods when useful.

Three: response writing stays in Apex. Endpoint methods return void. They call respond or error, which writes to RestContext.response.

Public API

Most app code extends Rest and overrides the method it needs.

MemberSignatureDescription
getpublic virtual void get(Rest.Request request)Handles GET; default returns method-not-allowed.
postpublic virtual void post(Rest.Request request)Handles POST; default returns method-not-allowed.
putpublic virtual void put(Rest.Request request)Handles PUT; default returns method-not-allowed.
patchpublic virtual void patch(Rest.Request request)Handles PATCH; default returns method-not-allowed.
delpublic virtual void del(Rest.Request request)Handles DELETE; default returns method-not-allowed.
canAccesspublic virtual Boolean canAccess(Rest.Request request)Endpoint access hook; defaults to true.
respondprotected void respond(Object body)Writes a JSON 200 response.
respondprotected void respond(Integer statusCode, Object body)Writes a JSON response with the given status.
errorprotected void error(Integer statusCode, String message)Writes a JSON error response.

Rest.Request exposes version, route, segments, primaryContext, secondaryContext, primaryId, secondaryId, ids, body, and params.

RestMock exposes seedConfigs(List<Rest_Config__mdt>) and seedSettings(Rest_Settings__c) for tests.

Notes & Edge Cases

  • Duplicate active Rest_Config__mdt records for the same route and version return a framework configuration error instead of picking one.
  • Route__c is a static root segment, not a regex.
  • Version__c is matched from URL segments like v1, v2, and v10.
  • JSON bodies that are blank or not JSON objects produce an empty body map.
  • Query ids win over body ids when both are present.
  • Sub-endpoint class names are derived from the path segment, so line-items resolves to an inner LineItems class.