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
- Is custom Apex REST actually required, or would the standard REST API, Composite API, or an invocable/action pattern be simpler?
- What authentication model and sharing behavior should the caller get? Apex REST "supports these authentication mechanisms: OAuth 2.0, Session ID" (apexdev L18402–18404) — there is no third option to design, only the two to configure.
- How will the URL, payload schema, and response schema evolve without breaking clients?
- What
apiVersionwill the class's.cls-meta.xmlcarry? That number, not the org's release, decides the sharing and user-mode defaults (apexdev L18735–18739). - Will this class ever ship inside a managed package? If so its public URL grows a namespace segment (apexdev L18465–18470), and the contract has to be published as configuration, not as a constant.
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.
| Ask | Why it matters | What 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-sensitive | One 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 method | A 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:
| Constraint | Grounded 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):
| Code | Platform meaning |
|---|---|
400 | An unhandled user exception occurred |
403 | You don't have access to the specified Apex class |
404 | The URL is unmapped in an existing @RestResource annotation / the extension is unsupported / the namespaced class couldn't be found |
405 | The request method doesn't have a corresponding Apex method |
406 | Content-Type set to something other than JSON or XML, or an unsupported header |
415 | Unsupported XML parameter type or Content-Type |
500 | An 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