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

  1. Build an endpoint URL against the REST API (/_api/...) rooted at the current site's absolute URL.
  2. Call this.context.spHttpClient.get(...) with the v1 configuration β€” SharePoint injects the caller's authentication for you.
  3. Parse JSON: list-item queries return { "value": [ ...items ] }.
  4. 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')/items targets a list by display name; $select, $orderby, $top are 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 + useEffect instead 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 catch surfaces 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 everything user-controlled: 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?

Further Reading