@RestResource annotation turns a standard Apex class into your very own custom API endpoint. You get to define the URL, the HTTP verbs, and the exact payload shape. It is the perfect tool when the standard Salesforce REST API is too generic or requires too many round-trips for a complex business process. Keep in mind: it runs under the context of the calling integration user, meaning their security permissions and sharing rules fully apply.
An enterprise ERP system was built to post new orders to a custom Salesforce REST endpoint. When the network was slow, the ERP would time out and automatically retry the request. Because the Apex endpoint was using a standard
insert statement, Salesforce ended up generating two identical orders for the same transaction.The Old/Bad Fix: The team built a nightly deduplication batch job to merge the duplicates. It was slow, error-prone, and occasionally merged genuine repeat orders from the same customer, destroying real business data.
The Modern/Good Fix: The team updated the endpoint to require a stable, unique Order Number from the ERP. Instead of inserting, the Apex code used
upsert against an External_Id__c. If the ERP retried a request, Salesforce simply updated the existing record. The duplicates vanished immediately, the dedup job was deleted, and the ERP didn't have to change a single line of client code.
๐ Key Points: Salesforce as the API Provider
When you use @RestResource, you are turning Salesforce into the API provider (often called a "Remote Call-In" pattern). Here is how it works under the hood:
- The Setup: Annotate your class with
@RestResource(urlMapping='/path/*')to expose it at/services/apexrest/path. - The Methods: Control the actions using annotations like
@HttpGet,@HttpPost,@HttpPut,@HttpPatch, and@HttpDelete. - Raw Access: Use
RestContext.requestandRestContext.responseto read raw headers, parameters, and payloads. - Context Matters: The code executes in the context of the authenticated calling user. This means Sharing, Field-Level Security (FLS), and CRUD permissions apply to that specific integration user.
- Modern Security: Always enforce user-mode operations (e.g., using
AccessLevel.USER_MODE), aggressively validate inputs against SOQL injection, and version your URL paths from day one (e.g.,/v1/orders).
๐งญ The 360 Card: Inbound Apex REST
Rule: @RestResource makes Salesforce the API provider. Always design it so a retry is perfectly safe (idempotent).
Gain: You control the exact contract—your URL, your verbs, your payload shape. It handles complex, multi-object logic that the standard REST API cannot do efficiently.
Price: You now own and maintain a custom API. Versioning, deprecation, and backward compatibility are entirely your responsibility.
Limits: It runs as the authenticated calling user. If they lack FLS to a field, your DML will fail unless properly handled. Using an upsert on an external ID is practically mandatory to safely handle network retries.
At Volume: Never build an endpoint that accepts a single record. Always design your endpoint to accept and process lists (arrays). An endpoint that takes one order at a time invites 10,000 separate HTTP calls, quickly blowing out your concurrent API limits.
๐ฌ Core Q&A & Interview Prep
Q: Design a custom inbound REST endpoint that lets an external system create Orders. What are the absolute must-haves?
Start with a class annotated with @RestResource(urlMapping='/orders/v1/*') and an @HttpPost method. Instead of manually parsing a messy Map, deserialize the JSON body directly into a strongly-typed Apex wrapper class using JSON.deserialize.
The endpoint must be Bulk-Safe—meaning it accepts a list of orders, not just one. It must be Safe to Retry (idempotent)—meaning it uses an upsert against a unique external key provided by the caller so retries don't create duplicates. Finally, it must enforce Modern Security—using with sharing and AccessLevel.USER_MODE so the code respects the integration user's exact profile limits.
// 1. Expose this class as a REST endpoint with a versioned URL.
@RestResource(urlMapping='/orders/v1/*')
global with sharing class OrderApi {
// 2. Map this method to POST requests.
@HttpPost
global static ResponseDto createOrders() {
RestRequest req = RestContext.request;
// 3. Turn the incoming JSON text into a list of strongly-typed Apex objects.
List<OrderDto> input = (List<OrderDto>) JSON.deserialize(
req.requestBody.toString(),
List<OrderDto>.class
);
List<Order__c> toUpsert = new List<Order__c>();
for (OrderDto d : input) {
// 4. Map to the Salesforce object using an External ID
toUpsert.add(new Order__c(
External_Id__c = d.extId,
Amount__c = d.amount
));
}
// 5. Upsert safely with modern User-Mode security (Idempotent on retry)
Database.upsert(toUpsert, Order__c.External_Id__c, AccessLevel.USER_MODE);
// 6. Return a proper HTTP status code
RestContext.response.statusCode = 201;
return new ResponseDto(toUpsert);
}
}
๐ Follow-Up Questions
Q1: The external caller sometimes retries on timeout, resulting in duplicate Orders in Salesforce. How do you fix this without asking them to change their client?
Make your endpoint safe to call twice (idempotent). Have the external system send a stable, unique key per transaction—like their internal Order Number. Instead of using a basic insert, use an upsert operation targeting an External_Id__c field. If a retry happens, Salesforce will recognize the key and simply update the existing record rather than creating a duplicate. Never assume exactly-once delivery across a network.
Q2: During a security review, an auditor asks: "This endpoint runs as which user, and can it access data it shouldn't?" How do you answer?
The endpoint runs exactly as the authenticated calling user—typically a dedicated Integration User assigned via an OAuth flow. This means their specific Profile and Permission Set define the maximum reach of the API. To guarantee data safety, ensure the Apex class is declared with sharing and utilize modern AccessLevel.USER_MODE for all DML operations. Never assign the Integration User broad "View All" or "Modify All" permissions just to make things work; that creates a massive data-exfiltration vulnerability.
Q3: Why is bulkifying the JSON payload so critical for Apex REST endpoints?
If you design an endpoint to accept a single JSON object (e.g., {"orderId": "123"}), an external system trying to sync 5,000 orders will have to make 5,000 separate HTTP POST requests. This will rapidly exhaust your org's 24-hour API request limits and cause concurrent Apex request timeouts. By expecting an array (e.g., [{"orderId": "123"}, {"orderId": "124"}]), the external system can send all 5,000 orders in just a handful of API calls, keeping your Salesforce org healthy and performant.