Real Data: Talking to SharePoint with SPHttpClient
Lesson 5: Real Data β Talking to SharePoint with SPHttpClient
A web part that prints "Hello" is a demo; a web part that reads the Announcements list is a tool. Reading data is where your component earns its place on the page β and it's remarkably direct: every web part gets a context with a pre-authenticated HTTP client pointed at SharePoint's REST API.
The data access pattern
- Build an endpoint URL against the REST API (
/_api/...) rooted at the current site's absolute URL. - Call
this.context.spHttpClient.get(...)with the v1 configuration β SharePoint injects the caller's authentication for you. - Parse JSON: list-item queries return
{ "value": [ ...items ] }. - Render into the DOM you created in
render()β and handle the failure case.
Here's a complete, minimal web part that fetches the ten newest items from a list called Announcements:
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { escape } from '@microsoft/sp-lodash-subset';
interface IAnnouncement { Id: number; Title: string; }
export default class AnnouncementsWebPart
extends BaseClientSideWebPart<{}> {
public render(): void {
this.domElement.innerHTML = `
<div class="awp">
<h2>Announcements</h2>
<div id="items">Loadingβ¦</div>
</div>`;
this._loadAnnouncements(); // fire the request, don't await in render
}
private _loadAnnouncements(): void {
const endpoint: string =
`${this.context.pageContext.web.absoluteUrl}` +
`/_api/web/lists/getbytitle('Announcements')/items` +
`?$select=Id,Title&$orderby=Created desc&$top=10`;
this.context.spHttpClient
.get(endpoint, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
if (!response.ok) { throw new Error('HTTP ' + response.status); }
return response.json();
})
.then((data: { value: IAnnouncement[] }) => {
const html = data.value
.map((it: IAnnouncement) => `<li>${escape(it.Title)}</li>`)
.join('');
const host = this.domElement.querySelector('#items');
if (host) { host.innerHTML = `<ul>${html}</ul>`; }
})
.catch((err: Error) => {
console.error('Announcements failed to load', err);
const host = this.domElement.querySelector('#items');
if (host) { host.textContent = 'Could not load announcements.'; }
});
}
protected get dataVersion(): Version { return Version.parse('1.0'); }
}
Why it's shaped this way
- REST endpoint:
lists/getbytitle('Announcements')/itemstargets a list by display name;$select,$orderby,$topare OData query options. Shape the query server-side β don't pull 5,000 rows and filter in the browser. - Async discipline:
render()must return immediately, so the fetch fires and the DOM updates when the promise resolves. (In a React web part you'd use state +useEffectinstead of querying#items.) - Identity: the request runs as the signed-in user β list permissions apply. A user who can't read the list gets an error, which your
catchsurfaces gracefully. - Loading + error states: the placeholder text and the catch block are not polish; they're what your users see on slow networks and broken configs. Always ship both.
escape() around Title above is non-negotiable β list items are content created by other people, and unescaped content in innerHTML is an XSS hole in your web part.
When you outgrow raw REST: PnPJS
Hand-written REST URLs get tedious fast (paging, batching, CRUD, error typing). The community-standard library PnPJS wraps the same APIs in a fluent, typed client:
import { spfi, SPFx } from '@pnp/sp';
import '@pnp/sp/webs';
import '@pnp/sp/lists';
const sp = spfi().using(SPFx(this.context));
const items = await sp.web.lists
.getByTitle('Announcements')
.items.select('Id', 'Title')
.orderBy('Created', false)
.top(10)();
// items: array of typed records β same data, less stringly-typed URL surgery
It's an npm dependency you add per-project (@pnp/sp + peer packages) and it's what most production samples in the wild use β you'll see it constantly in the example galleries from Lesson 11. Learning raw SPHttpClient first is still the right move: it makes what PnPJS does for you legible.
π§ Knowledge Check
1. What does the REST call /_api/web/lists/getbytitle('Announcements')/items?$top=10 return?
2. Whose permissions apply when a web part calls SPHttpClient?
3. Why shouldn't render() await the data fetch synchronously?