Microsoft Graph is the strategic API for all of Microsoft 365, SharePoint included. It exposes sites, lists, list items, and document libraries under a single endpoint, with one authentication model and one set of conventions. If you're building a service that touches SharePoint and users, groups, Teams, or mail, Graph lets you do it all through one API instead of stitching several together. The catch is its addressing model — once you internalise how Graph identifies a site, the rest falls into place.

The SharePoint surface of Graph

Everything lives under https://graph.microsoft.com/v1.0. The SharePoint entities you'll use most:

The first hurdle: resolving a site ID

Graph doesn't address sites by URL — it uses a composite site ID of the form {hostname},{site-collection-guid},{web-guid}. You rarely know that up front, so you resolve it from the friendly URL using the :/sites/... path syntax:

GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/ops
# → returns the site, whose "id" is the composite you use everywhere else

# Root site of the tenant:
GET https://graph.microsoft.com/v1.0/sites/root
Resolve the site ID once and cache it. The composite ID is stable for the life of the site, so there's no reason to pay for the lookup on every operation. Caching it also removes a round-trip from every code path that touches the site.

Working with lists and items

Once you have the site ID, lists and items use the OData query options you already know. Note that item columns live under the fields facet, which you expand:

# All lists on a site
GET /sites/{site-id}/lists

# Items with selected columns (fields must be expanded)
GET /sites/{site-id}/lists/{list-id}/items
    ?$expand=fields($select=Title,ContractStatus,Modified)
    &$top=50

# Filter on a column (header required for non-indexed filters)
GET /sites/{site-id}/lists/{list-id}/items?$expand=fields
Headers: Prefer: HonorNonIndexedQueriesWarningMayFailRandomly

Creating and updating items

POST /sites/{site-id}/lists/{list-id}/items
{
  "fields": {
    "Title": "Acme MSA 2021",
    "ContractStatus": "Draft"
  }
}

# Update just the fields of an existing item
PATCH /sites/{site-id}/lists/{list-id}/items/{item-id}/fields
{ "ContractStatus": "Approved" }

Files via the drive

Document libraries surface as drives, so file operations reuse the entire OneDrive/Graph files API — upload, download, move, share, thumbnails, and more:

# List the root folder of the default library
GET /sites/{site-id}/drive/root/children

# Upload a small file (< 4 MB) by path
PUT /sites/{site-id}/drive/root:/Contracts/acme.pdf:/content
Content-Type: application/pdf
<binary>

# Download
GET /sites/{site-id}/drive/items/{item-id}/content

For large files you create an upload session and send the bytes in chunks — the same resumable pattern across all of Graph's file storage.

Delta queries: efficient sync

If you're keeping an external system in step with a list or library, polling everything is wasteful. Delta query returns only what changed since your last call. You make an initial delta call, follow @odata.nextLink through the pages, and store the @odata.deltaLink you get at the end. Next time, call the deltaLink and you receive only the changes:

# Initial sync — walk nextLink pages to the end
GET /sites/{site-id}/lists/{list-id}/items/delta?$expand=fields
# … pages …
# Final page returns: "@odata.deltaLink": ".../delta?token=ABC123"

# Later — only what changed since that token
GET /sites/{site-id}/lists/{list-id}/items/delta?token=ABC123
Delta query is Graph's answer to the CSOM GetChanges/change-token pattern. Persist the deltaLink exactly as you'd persist a change token, and your sync becomes incremental and cheap — even against large libraries. Deleted items come back marked with an @removed facet so you can mirror deletions too.

Throttling: design for 429

Graph throttles aggressively under load and returns 429 Too Many Requests with a Retry-After header. Production code must honour it:

var res = await graph.Sites[siteId].Lists[listId].Items
    .Request().GetAsync();   // SDK retries 429 with Retry-After by default

// If hand-rolling HTTP, respect the header yourself:
if ((int)response.StatusCode == 429)
{
    var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(10);
    await Task.Delay(wait);
    // retry
}
Never ignore Retry-After. Hammering through a 429 gets your app throttled harder and can affect the whole tenant. Use the official Graph SDKs where you can — they implement compliant retry/back-off out of the box — and batch with /$batch to cut request counts.

Graph vs the SharePoint REST API

The trajectory is clear: Graph keeps absorbing more SharePoint surface every release, and it's where Microsoft invests. Build new integrations on Graph, fall back to _api only where you must, and you'll have a solution that ages well and reaches the entire Microsoft 365 platform from a single, consistent API.

DS

Dhaval Shah

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