CSOM is great when you are writing managed code, but the moment you are inside a browser — a content editor web part, a JSLink override, or just an HTML page — the REST API is your gateway to SharePoint. It is plain HTTP and OData, which means anything that can make a web request can read and write SharePoint data. This article covers the mechanics that trip people up: the request digest, response shaping, and keeping payloads lean.

The shape of an endpoint

Every REST call hangs off the site's _api endpoint. The URL is the query:

GET https://contoso.sharepoint.com/sites/ops/_api/web/title
GET .../_api/web/lists/getbytitle('Contracts')
GET .../_api/web/lists/getbytitle('Contracts')/items

The two namespaces you'll live in are /_api/web/... (lists, items, files, folders) and /_api/site/... (site-collection scope). Reading is a simple GET; the interesting part is writing.

Reading items and shaping the response

A naïve GET .../items returns every column of every item, plus a wall of OData metadata. Three query options fix that:

GET .../_api/web/lists/getbytitle('Contracts')/items
    ?$select=Id,Title,ContractStatus,Author/Title
    &$expand=Author
    &$filter=ContractStatus eq 'Approved'
    &$orderby=Modified desc
    &$top=50
Accept: application/json;odata=nometadata
The Accept: application/json;odata=nometadata header is the easiest payload win there is. The default verbose level wraps everything in d.results and bloats responses with type metadata; nometadata gives you a clean flat object and a top-level value array.

Expanding lookups correctly

A lookup or person field returns an ID by default. To get the friendly value you must $expand the related entity and $select its sub-property — $expand=Author&$select=Author/Title. Forgetting the $select sub-property is the number-one reason people see [object Object] instead of a name.

The request digest: writing data

SharePoint protects against cross-site request forgery by requiring a form digest token on every state-changing call (POST, MERGE, DELETE). On a SharePoint page the token already sits in a hidden field, #__REQUESTDIGEST. From a standalone context you fetch it from /_api/contextinfo:

// Get a fresh digest (valid for ~30 minutes)
async function getDigest(webUrl) {
    const res = await fetch(`${webUrl}/_api/contextinfo`, {
        method: "POST",
        headers: { "Accept": "application/json;odata=nometadata" }
    });
    const data = await res.json();
    return data.FormDigestValue;
}

Creating an item

To create, POST to the items collection. The body must specify the list item entity type, which you can read from the list's ListItemEntityTypeFullName property:

async function createContract(webUrl, digest) {
    const body = {
        "__metadata": { "type": "SP.Data.ContractsListItem" },
        "Title": "Acme MSA 2017",
        "ContractStatus": "Draft"
    };

    const res = await fetch(
        `${webUrl}/_api/web/lists/getbytitle('Contracts')/items`, {
        method: "POST",
        headers: {
            "Accept": "application/json;odata=verbose",
            "Content-Type": "application/json;odata=verbose",
            "X-RequestDigest": digest
        },
        body: JSON.stringify(body)
    });
    return res.json();
}
The entity type name follows the pattern SP.Data.<ListName>ListItem, where the list name is the list's internal (URL) name with spaces removed. When in doubt, GET .../lists/getbytitle('X')?$select=ListItemEntityTypeFullName and read it directly — don't guess.

Updating and deleting

Updates use the MERGE verb (tunnelled through X-HTTP-Method) so you only send changed fields, plus an IF-MATCH ETag for optimistic concurrency. Use "*" to force the write, or the item's real ETag to fail if someone changed it first:

// Update item 42 — only the Status field
await fetch(`${webUrl}/_api/web/lists/getbytitle('Contracts')/items(42)`, {
    method: "POST",
    headers: {
        "Accept": "application/json;odata=nometadata",
        "Content-Type": "application/json;odata=verbose",
        "X-RequestDigest": digest,
        "X-HTTP-Method": "MERGE",
        "IF-MATCH": "*"
    },
    body: JSON.stringify({
        "__metadata": { "type": "SP.Data.ContractsListItem" },
        "ContractStatus": "Approved"
    })
});

// Delete item 42
await fetch(`${webUrl}/_api/web/lists/getbytitle('Contracts')/items(42)`, {
    method: "POST",
    headers: {
        "X-RequestDigest": digest,
        "X-HTTP-Method": "DELETE",
        "IF-MATCH": "*"
    }
});

Batching with $batch

Just like CSOM, chatty REST is slow REST. The OData $batch endpoint lets you bundle many reads and writes into one HTTP request using a multipart body. For more than a handful of operations it is a dramatic win — and libraries like PnPjs wrap it in a clean API so you rarely hand-craft the multipart payload yourself.

Cross-domain and CORS

The REST API does not enable CORS, so you cannot call one tenant's _api directly from a page on another origin. Inside an add-in, the SP.RequestExecutor cross-domain library proxies calls through the app web. From server-side code, you authenticate with OAuth and call REST directly. Knowing which context you are in determines how you authenticate.

Why this still matters

The SharePoint REST API remains the most direct way to manipulate list data from the browser, and it underpins higher-level libraries you will use daily. Even as Microsoft Graph becomes the strategic API for Microsoft 365, plenty of SharePoint-specific operations are still only available — or simply easier — through _api. Master $select, $expand, the digest, and odata=nometadata, and you can talk to SharePoint from anywhere.

DS

Dhaval Shah

Cloud Solution Architect — 18+ years across SharePoint, Microsoft 365, and Azure AI.