SfSkills Domains Docs GitHub

Home / apex / apex-rest-services

apex-rest-services

Use when building, reviewing, or debugging inbound Apex REST resources, request/response handling, status codes, versioned URL mappings, or JSON serialization in `@RestResource` classes. Triggers: 'Apex REST', '@RestResource', 'HttpGet/HttpPost', 'RestContext', 'versioned endpoint', 'services/apexrest', 'urlMapping', 'RestRequest requestBody', 'RestResponse statusCode'. NOT for outbound callouts — use apex/callouts-and-http-integrations. NOT for a deprecation and sunset policy across many endpoints — use integration/api-versioning-strategy.

Open the skill folder on GitHub


Use this skill when Salesforce itself is exposing a custom HTTP contract through Apex. The point is to keep the REST resource thin, explicit about sharing and validation, and predictable in its status codes and JSON behavior. A custom endpoint should be a deliberate choice, not the default substitute for standard APIs or internal Apex calls.


Before Starting

Questions to Ask Before Configuring

Ask these before writing the class; the answers decide the contract, and an LLM that skips them ships an endpoint that deploys, passes its tests, and returns 200 on failure.

AskWhy it mattersWhat a good answer adds
"What is the exact URL, including the version segment, and who else may claim it?"Overlapping mappings resolve to whichever class was saved first, not to the more specific one (gotcha: wildcard/save order) — and the mapping is case-sensitiveOne mapping string, published verbatim, that no other @RestResource in the org can also match
"What is the complete list of status codes this endpoint may return, ours and the platform's?"A code outside the documented statusCode table becomes a 500; unhandled exceptions become a platform 400/500 that never reaches your envelope (gotchas: status-code whitelist; unhandled exceptions)A status table the client can code against, splitting codes you set from codes the platform sets
"What does the caller need to see when it fails — a field name, a retryable flag, a support reference?"Decides the error envelope's fields; a raw exception message leaks internals, and without a correlation id a client complaint cannot be resolved to a log row (gotcha: unhandled exceptions bypass the envelope)The envelope schema, plus the Application_Log__c query that resolves a client-quoted request id
"Which permission set grants Apex Class Access, and to which class?"Class security is checked at the entry point only; without it every call is 403 before your code runs (gotcha: Apex Class Access)A permission set shipped in the same package.xml, granting the resource class and not the service class
"Is the response bounded — one record, a page, or 'all of them'?"Heap exhaustion during serialization returns 200 with a truncated body, so an unbounded collection endpoint fails invisibly (gotcha: heap/200)A page size in the contract and a decision to return void + RestResponse rather than an sObject collection
"Does any method need the raw body — for a signature check, replay, or an idempotency key?"Declaring method parameters empties RestRequest.requestBody (gotcha: parameters vs requestBody), and the two styles cannot be mixed in one methodA per-method decision: parameterless + explicit JSON.deserialize, or parameter-based deserialization
"What is the slowest realistic call, and how large is the largest payload?"Requests over 20 seconds consume a concurrency slot capped at 25 (5 in DE); body is capped at 6 MB sync, and URI+headers at 16,384 bytes (gotcha: size ceilings)A published maximum batch size, filters moved into the POST body, and an async fallback if p99 approaches 20 s

What a proper configuration adds over just writing a @RestResource class: the client can branch on the status code instead of parsing prose, a support ticket resolves to one log row through the request id, the endpoint's access posture is readable in the source rather than implied by an apiVersion pin, and the failure modes the platform decides for you are documented alongside the ones you decided.


Core Concepts

@RestResource Is An Adapter Layer

A REST resource should parse the request, validate inputs, delegate to a service, and shape the response. It should not become the only place business logic lives. Thin resources are easier to version, easier to secure, and easier to test by setting RestContext.request and RestContext.response.

The smallest resource that keeps the contract in its own hands looks like this — global class, global static void verb method, no parameters, and every exit path writing both halves of the response:

@RestResource(urlMapping='/v1/cases/*')
global with sharing class CaseApiV1 {
    @HttpGet
    global static void doGet() {
        RestResponse res = RestContext.response;
        res.statusCode  = 200;                      // never left at the default
        res.addHeader('Content-Type', 'application/json');
        res.responseBody = Blob.valueOf(JSON.serialize(
            new Map<String, Object>{ 'id' => '500...', 'status' => 'New' }, true
        ));
    }
}

The platform constrains the shape of that adapter:

ConstraintGrounded statement
Class must be global"To use this annotation, your Apex class must be defined as global" (apexdev L6352)
Methods must be global static"To use this annotation, your Apex method must be defined as global static" (apexdev L6388, restated per verb)
One method per verb"A single Apex class annotated with @RestResource can't have multiple methods annotated with the same HTTP request method" (apexdev L18446–18448)
@HttpGet / @HttpDelete take no parameters"GET and DELETE requests have no request body, so there's nothing to deserialize" (apexdev L18442–18443)
@HttpGet also answers HEAD"Methods annotated with @HttpGet are also called if the HTTP request uses the HEAD request method" (apexdev L6389)
Mapping is relative and slash-prefixed"The URL mapping is relative to https://instance.salesforce.com/services/apexrest/"; "The path must begin with a forward slash (/)"; "The path can be up to 255 characters long" (apexdev L6348, L6357–6358)
No multipart/form-data"Apex REST currently doesn't support requests of Content-Type multipart/form-data" (apexdev L18449)

Allowed parameter and return types are a closed list: "Apex primitives (excluding sObject and Blob) · sObjects · Lists or maps of Apex primitives or sObjects (only maps with String keys are supported) · User-defined types that contain member variables of the types listed above" (apexdev L18425–18437). For a user-defined type, "Apex REST deserializes request data into public, private, or global class member variables of the user-defined type, unless the variable is declared as static or transient" (apexdev L18488–18490) — and "the names of the Apex parameters matter, although the order doesn't" (apexdev L18570).

Status Codes And Error Bodies Are Part Of The Contract

If an endpoint always returns 200 with a loosely structured body, clients cannot behave reliably. Set explicit status codes, return consistent JSON error shapes, and distinguish validation failures, not-found cases, and server-side exceptions.

Two halves of the status contract are not yours. The platform sets these before or around your code (apexdev L18680–18696):

CodePlatform meaning
400An unhandled user exception occurred
403You don't have access to the specified Apex class
404The URL is unmapped in an existing @RestResource annotation / the extension is unsupported / the namespaced class couldn't be found
405The request method doesn't have a corresponding Apex method
406Content-Type set to something other than JSON or XML, or an unsupported header
415Unsupported XML parameter type or Content-Type
500An unhandled Apex exception occurred

And the codes you may set are a whitelist, not a range: 200, 201, 202, 204, 206, 300, 301, 302, 304, 400, 401, 403, 404, 405, 406, 409, 410, 412, 413, 414, 415, 417, 500, 503 (apexrefguide L228772–228820). 422 and 429 are not on it.

Version The URL Mapping Deliberately

Versioning should be visible and explicit, often in the URL mapping or path contract. This keeps older consumers from being broken by incompatible payload changes. Versioning is an operational strategy, not just a naming convention.

Mapping resolution is deterministic but unintuitive: "An exact match always wins. If no exact match is found, find all the patterns with wildcards that match, and then select the longest (by string length) of those. If no wildcard match is found, an HTTP response status code 404 is returned" (apexdev L6362–6364). A wildcard is not restricted to the tail — "unless the wildcard is the last character in the path, it must be followed by a forward slash (/)" (apexdev L6360–6361) — so /v1/cases/*/comments is legal. Where two classes still overlap, save order decides (apexdev L18589–18591).

Security Must Be Declared And Enforced

REST classes still need explicit sharing decisions and secure data access patterns. Authentication into Salesforce is only one half of the problem; the Apex code must still enforce the right record, object, and field boundaries. Never infer that boundary from the org's release: what a resource class does with no sharing keyword and no query clause is set by the apiVersion in its .cls-meta.xml, and it inverted at 67.0 (Summer '26). Read 2 § Apex security idiom by API version before scoring an endpoint's posture.

The guide states both halves of the 67.0 change and the Apex-REST-specific default:

Bypassing is now the thing that must be explicit: "To bypass object or field-level security while using SOQL SELECT statements in Apex, you must use the WITH SYSTEM_MODE clause"; "To bypass sharing rules for Apex REST API methods, you must explicitly declare the class that contains these methods with the without sharing keyword" (apexdev L18727–18732).


Common Patterns

Thin Resource + Service Layer

When to use: A custom endpoint handles business logic on Salesforce data.

How it works: Parse request data in the resource class, delegate the real workflow to a service, then set RestContext.response deliberately. Worked end to end in references/code-examples.md (CaseApiV1 + CaseApiService), with templates/apex/BaseService.cls supplying the savepoint and logging contract.

Why not the alternative: Fat resource classes are hard to version and tend to hide security mistakes. There is also a permissions reason: class security is checked at the entry point, so the resource is the class that goes in the permission set and the service is the class that does not (apexdev L12323–12329).

Consistent Error Envelope

When to use: Consumers need stable machine-readable errors.

How it works: Return a simple JSON structure with code, message, and optional correlation ID. Use System.Request.getCurrent().getRequestId() as that ID — it is "the same as in the event log files of the Apex Execution event type used in Event Monitoring" (apexrefguide L228153), so one value joins the client's complaint, the org's log row, and the Event Monitoring record.

Why not the alternative: A prose message forces every client to string-match, and string-matching breaks on the first wording change.

Versioned URL Mapping

When to use: The API contract may evolve incompatibly.

How it works: Include versioning in the URL contract and keep old behavior stable until clients migrate. One class per version, each with a mapping no other class can match, so a v2 rollout is a new class rather than an edit to a live one.

void Method + Explicit RestResponse

When to use: Any endpoint with a published contract — which is all of them.

How it works: Declare the method global static void, take no parameters, read RestContext.request.requestBody, and write res.statusCode and res.responseBody yourself.

Why not the alternative: A non-void return type hands serialization to the platform, which drops null fields (apexdev L18463–18465), emits the sObject attributes envelope with a version-pinned URL (apexdev L18818–18828), and — on a large collection — can exhaust heap after the 200 header has already gone out (apexdev L18474–18477).


Decision Guidance

SituationRecommended ApproachReason
External system needs a custom business API in SalesforceApex RESTSalesforce exposes a deliberate custom contract
Consumer only needs standard data CRUD/query accessPrefer standard Salesforce APIsLess code and less custom maintenance
Contract may change incompatibly over timeVersioned URL mapping and payload contractSafer client evolution
Endpoint updates Salesforce dataThin REST resource + secure service layerBetter maintainability and security review
Endpoint returns a collection that could growvoid + explicit RestResponse + a page size in the contractHeap exhaustion mid-serialization returns 200 with a truncated body (apexdev L18474–18477)
Validation failure needs its own status400 with a code in the envelope — never 422422 is absent from the statusCode table and is converted to 500 (apexrefguide L228768–228770)
Read-only reporting endpoint over a very large table@ReadOnly on the methodRaises the returned-row limit to 1,000,000, but "blocks… DML operations, calls to System.schedule, and enqueued asynchronous Apex jobs" (apexdev L6162–6164)
Caller needs to upload a binary fileParameterless method reading req.requestBody as a BlobThe guide's attachment sample does exactly this (apexdev L18867–18886); multipart/form-data is unsupported (apexdev L18449)
Payload will exceed a few MB or the call will run past 20 sMove to Bulk API 2.0 or an async pattern6 MB sync / 12 MB async request-or-response cap (apexdev L18396–18397); 25 concurrent long-running requests (App Limits L481–493)
Outbound call from Salesforce to another systemapex/callouts-and-http-integrationsThis skill is inbound only

  1. Answer the questions above and freeze the contract. Fix the mapping string (version segment included), the verb-to-method map, the request and response DTOs, the error envelope fields, and the full status table — yours and the platform's from Core Concepts.
  2. Check the mapping is unclaimed. Grep the source tree for urlMapping and confirm nothing else matches the path, including the /x vs /x/* overlap; the resolution rules are in references/gotchas.md.
  3. Write the pair from references/code-examples.md. Copy CaseApiV1.cls and CaseApiService.cls, keep every method global static void with no parameters, and route CRUD/FLS through templates/apex/SecurityUtils.cls and failure logging through templates/apex/ApplicationLogger.cls rather than hand-rolling either.
  4. Wire the deployment artifacts. Take the *.cls-meta.xml (<apiVersion>67.0</apiVersion>) and the package.xml from the same file. The package.xml must include the permission set granting Apex Class Access on the resource class — without it every call is 403.
  5. Write the test from the same file. CaseApiV1Test builds RestRequest / RestResponse, assigns RestContext.request and RestContext.response, and asserts on statusCode, on the envelope's code, and on the Location header — not just on the record count.
  6. Run the checker. python3 skills/apex/apex-rest-services/scripts/check_apex_rest_services.py --manifest-dir force-app — it flags a non-global resource class or non-global static verb method, duplicate verb annotations, an illegal or duplicated urlMapping, a missing sharing declaration graded against the .cls-meta.xml, @HttpGet/@HttpDelete with parameters, a status code outside the documented table, a catch block that sets no status code, and a resource with no test class touching RestContext.
  7. Deploy, then probe the contract. sf project deploy start -x manifest/package.xml, sf apex run test -n CaseApiV1Test -r human -w 20 -c -y, then the cURL probes in references/code-examples.md § 7 — including the two that must return 405 and 404, which prove the mapping is what you think it is.

Review Checklist

Salesforce-Specific Gotchas

The full list with What happens / When it occurs / How to avoid is in references/gotchas.md. The four that most often survive review:

  1. RestContext is only populated in REST execution or tests that set it — do not assume it exists in ordinary Apex contexts. RestRequest() and RestResponse() are public constructors and RestContext.request / .response are read-write properties, which is the whole test mechanism (apexrefguide L228308–228340, L228428, L228696).
  2. Apex REST security is not automatic beyond authentication — data access still needs explicit review, and the class's apiVersion decides the default.
  3. Returning raw exceptions as response bodies leaks internals — map failures to stable error contracts, and remember that unhandled exceptions bypass your envelope entirely as a platform 400 or 500 (apexdev L18684, L18693).
  4. Resource classes become brittle fast if business logic lives directly inside them — versioning pain follows, and the permission-set boundary blurs.

Output Artifacts

ArtifactDescription
REST endpoint reviewFindings on contract design, status handling, security, and versioning
Resource scaffoldThin @RestResource class with explicit request parsing and response shaping
Service classThe business half, extending templates/apex/BaseService.cls, with CRUD/FLS via templates/apex/SecurityUtils.cls
Test classRestContext-driven coverage asserting status codes, envelope code, and response headers
*.cls-meta.xml + package.xmlDeployment metadata at <apiVersion>67.0</apiVersion>, including the Apex Class Access permission set
Contract probe scriptcURL calls that assert the happy path plus the platform's 404/405 for unmapped URL and missing verb
Versioning notesGuidance for evolving the endpoint without breaking consumers

Reference Files

FileRead it when
references/code-examples.mdYou are writing the endpoint — resource + service + DTOs + test class, .cls-meta.xml, package.xml, sf apex run test, and the cURL contract probes
references/gotchas.mdAn endpoint deployed but 404s, returns 500 for a code you set, returns 200 on failure, or works in the dev org and not after packaging — 15 grounded platform behaviours
references/examples.mdYou want the narrative walk-through of a versioned GET and a typed POST before writing code
references/llm-anti-patterns.mdYou are reviewing AI-generated @RestResource code, or self-checking your own output
references/well-architected.mdYou are justifying a custom endpoint over the standard APIs, or need the source list
templates/apex-rest-services-template.mdYou are filling in the contract worksheet before any code exists

Package files