---
title: "Designing an Apex REST endpoint"
url: "https://trailassembly.com/apex-rest-endpoint-design/"
topic: "Data and Integration"
published: "2026-09-11T14:27:44+00:00"
updated: "2026-10-06T13:53:11+00:00"
---

# Designing an Apex REST endpoint

**Straight answer:** `@RestResource` gives you a URL under `/services/apexrest/` and six annotations for the HTTP verbs. The class must be `global`, the methods `static`, and the URL mapping unique. Everything after that is design, and the design decisions that matter are authentication, bulk, and what you do when half the payload is bad.

## The minimum

```apex
@RestResource(urlMapping='/orders/*')
global with sharing class OrderApi {

    @HttpGet
    global static Order__c getOne() {
        RestRequest req = RestContext.request;
        String ref = req.requestURI.substring(req.requestURI.lastIndexOf('/') + 1);
        List<Order__c> found = [
            SELECT Id, Reference__c, Status__c FROM Order__c
            WHERE Reference__c = :ref WITH USER_MODE LIMIT 1
        ];
        if (found.isEmpty()) {
            RestContext.response.statusCode = 404;
            return null;
        }
        return found[0];
    }
}
```

Two things worth noticing. The URL wildcard means you parse the path yourself. There is no routing layer. And returning null with a status code is how you produce a 404, because the annotation has no other mechanism.

## Accept a list, always

The single most common design mistake is an endpoint that creates one record. The caller then loops, and every call is a separate transaction, a separate API call against your daily limit, and a separate round trip. An endpoint that accepts a list of up to a few hundred is barely more code and an order of magnitude better to consume.

```apex
@HttpPost
global static List<ItemResult> create(List<OrderInput> orders) {
    ...
}
```

Apex deserialises the request body into your parameters by name, so a JSON body of `{"orders": [ ... ]}` maps to a parameter called `orders`. This is convenient and it is also why a rename breaks every caller silently.

## Partial success, and saying which half failed

A list endpoint that throws on the first bad row wastes the good ones. Use partial-success DML and return a result per input:

```apex
Database.SaveResult[] saves = Database.insert(records, false);
List<ItemResult> out = new List<ItemResult>();
for (Integer i = 0; i < saves.size(); i++) {
    ItemResult r = new ItemResult();
    r.reference = inputs[i].reference;
    r.success   = saves[i].isSuccess();
    r.errors    = saves[i].isSuccess() ? null
                : String.join(errorStrings(saves[i].getErrors()), '; ');
    out.add(r);
}
return out;
```

Then return 200 with a body describing what happened, or 207 if you want to be strict about it. Returning 500 because one row had a bad picklist value is technically defensible and practically unhelpful.

## Security is not automatic

The class being `global` does not make it public — the caller still needs a session and the right permissions. What it does not give you for free:

- **Object and field permissions.** Add `WITH USER_MODE` on queries and `AccessLevel.USER_MODE` on DML, as in [enforcing CRUD and FLS](https://trailassembly.com/apex-enforcing-crud-and-fls/).

- **Sharing.** Declare `with sharing` on the class. A REST class with no keyword runs without sharing, which is how an endpoint quietly becomes an access bypass.

- **Input validation.** Anything from the body is caller-controlled. A reference used in dynamic SOQL is an injection risk exactly as described in [preventing SOQL injection](https://trailassembly.com/apex-soql-injection/).

## Idempotency for anything that creates

Callers retry. Networks time out after the work succeeded. If your POST creates a record every time it is called, a retry creates a duplicate. Accept a caller-supplied key, store it on an [external ID](https://trailassembly.com/upsert-with-external-ids/) field, and upsert. Then a retry is harmless and you have removed the hardest class of integration bug.

## Versioning

Put the version in the URL mapping, `/v1/orders/*`, from the first release, even when there is only one version. Retrofitting it later means either breaking callers or maintaining an unversioned path forever.

## Frequently asked

**Can a method be `public`?** No, the class and the annotated methods must be `global`.

**How do I return a custom status code?** Set `RestContext.response.statusCode`. An uncaught exception returns 500 with a body you did not write, so catch and translate.

**What is the payload size limit?** 6 MB for the request in a synchronous context, and the response counts against heap. A large batch is a heap problem before it is a design problem.

---

Source: https://trailassembly.com/apex-rest-endpoint-design/
Trail Assembly — independent Salesforce write-ups.
