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:
/sites/{site-id}— a site (web)./sites/{site-id}/lists/{list-id}— a list, and/itemsbeneath it./sites/{site-id}/driveand/drives/{drive-id}— document libraries as OneDrive-style drives, with/itemsand/root/children.
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
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
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
}
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
- Reach for Graph when your solution spans Microsoft 365, when you want one auth model, or when you need delta sync and the files API.
- Reach for
_apiwhen you need a SharePoint-specific capability Graph doesn't expose yet (some field/schema operations, certain site administration, search nuances).
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.