Skip to content

SharePoint

SPFx + PnPjs: Complete Developer Guide

Use PnPjs in SPFx with current patterns for lists, CRUD, filtering, paging, batching, files, users, groups, and search — behind a service layer.

Suresh Girinathuni
Published
Reading time
21 min read
SPFx PnPjs guide illustration showing service calls with select, filter, top, and paged queries

What you’ll learn

  • What is PnPjs
  • PnPjs versus SharePoint REST
  • PnPjs versus Microsoft Graph
  • Installation
  • Initialize PnPjs in SPFx

Direct answer: SPFx solutions frequently need to read and update SharePoint data. SharePoint REST can be called directly, but PnPjs provides a typed JavaScript and TypeScript API that simplifies many common SharePoint and Microsoft 365 scenarios. Using PnPjs does not automatically create a good architecture, however — production solutions still need service separation, efficient queries, paging, error handling, permission handling, data-volume planning, security, and testing.

Use PnPjs to simplify API access — not to move all data logic into React components. Every pattern in this guide routes through the same layers:

  1. React or SPFx component — renders props and state.
  2. Service layer — typed operations behind interfaces.
  3. PnPjs — fluent calls over supported APIs.
  4. SharePoint — lists, items, libraries, files, folders, users, groups, search.

For UI architecture, use SPFx and React; for the full lifecycle, the SPFx complete guide; for navigation, the SPFx hub.

What is PnPjs

PnPjs is an open-source, community-maintained Microsoft 365 developer library with a fluent, TypeScript-friendly API over SharePoint APIs and — through its graph packages — parts of Microsoft Graph. Understand what it is not: not a separate SharePoint database, not a migration tool, not a security bypass, not a replacement for SharePoint permissions, and not a universal replacement for Microsoft Graph or every REST endpoint. It simplifies calls; authorization still comes entirely from SharePoint and Microsoft Entra ID.

PnPjs versus SharePoint REST

Raw REST means the SPFx HTTP client, a hand-built endpoint URL, and manual response parsing. PnPjs means the same call expressed fluently with types:

RAW REST:   SPFx → HTTP Client → REST URL → Parse Response
PNPJS:      SPFx → PnPjs → Fluent API → SharePoint

PnPjs ultimately works with Microsoft 365 APIs — it creates no new platform permissions and adds no new capabilities beyond those APIs. REST knowledge stays valuable for debugging, for operations PnPjs does not wrap, and for understanding what the fluent call actually sends over the wire.

PnPjs versus Microsoft Graph

Reach for PnPjs for convenient SharePoint operations and for Microsoft Graph for cross-Microsoft 365 APIs — with the honest middle ground that PnPjs also ships Graph capabilities depending on current package and API design. Neither implication holds: PnPjs is not SharePoint-only, and Graph does not replace all SharePoint APIs. Deep Graph integration belongs in the future dedicated Graph article; this guide uses Graph only where PnPjs meets it.

Installation

Install only the packages the solution uses — never the entire PnPjs ecosystem by habit. The documented baseline for SharePoint plus Graph work is:

npm install @pnp/sp @pnp/graph --save

Verify exact package names before publishing, because package boundaries move between PnPjs major versions. Import selectively per module (lists, items, files, and so on) so bundlers can tree-shake what the solution never calls.

Initialize PnPjs in SPFx

The current recommended pattern creates an SPFI instance bound to the web part context — verified against official PnP documentation, identical across PnPjs v3 and v4 for getting started:

import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import type { WebPartContext } from "@microsoft/sp-webpart-base";

export function createSharePoint(
  context: WebPartContext
): ReturnType<typeof spfi> {
  return spfi().using(SPFx(context));
}

Initialization happens once per service, constructed with the web part context — so calls run as the current user with no secrets involved. When SharePoint and Graph packages are used together, alias the behavior imports per the official guidance instead of colliding them.

  1. SPFx context — user, site, and clients from the web part.
  2. SPFI — the configured PnPjs instance.
  3. PnPjs — fluent SharePoint operations.
  4. SharePoint — data behind approved access.

Avoid reinitializing PnPjs everywhere

Components creating their own SPFI instances scatter configuration, multiply identities to reason about, and defeat testing. Prefer one reusable initialization flowing web part, SPFI instance, services, components — constructed once, injected as a dependency, mocked in tests. Do not enforce singleton dogma blindly: per-service instances with clear ownership beat a global that every module mutates.

AVOID:    Component A → create SPFI
          Component B → create SPFI
          Component C → create SPFI

PREFER:   SPFx Web Part → SPFI Instance → Services → Components

Service-layer architecture

The central discipline: React components and hooks consume service interfaces; service implementations call PnPjs. This buys maintainability, testability, reuse, centralized error handling, and separation of concerns that survives team changes:

AVOID:    React Component → PnPjs Calls Everywhere → SharePoint

PREFER:   React Component → Hook → IProjectService
          → ProjectService → PnPjs → SharePoint

Depth: SPFx and React for the UI side of this contract.

Service interface

Components depend on operations, never on data sources. An architecture-oriented project service shows the shape — realistic project operations, not demo data:

import type { IProject, INewProject } from "../models/IProject";

export interface IProjectService {
  getProjects(): Promise<IProject[]>;
  getProject(id: number): Promise<IProject | undefined>;
  createProject(project: INewProject): Promise<number>;
  updateProject(id: number, changes: Partial<IProject>): Promise<void>;
}

Data models

Map SharePoint items to application models at the service boundary so UI components never couple to raw response shapes:

SharePoint Item → Service Mapping → Application Model → React
export interface IProject {
  id: number;
  title: string;
  status: string;
  ownerName: string;
  modified: string;
}

export interface INewProject {
  title: string;
  status: string;
}

When the list schema evolves, one mapping function changes instead of every component that renders a title.

Get a list

Reference lists by title for readability or by ID for stability — stable IDs survive renames, which matters in environment-aware solutions where titles drift between tenants. Never hard-code production identifiers without a configuration story; titles belong in web part properties or configuration, IDs in environment configuration:

const byTitle = sp.web.lists.getByTitle(listTitle);
const byId = sp.web.lists.getById(listId);

Get list items

Retrieve a Projects list — Id, Title, Status, Owner, Modified — selecting required fields, filtering server-side, ordering, and paging from the first query. Never fetch every field and every item to sort out later in React:

const items = await sp.web.lists
  .getByTitle("Projects")
  .items.select("Id", "Title", "Status", "Author/Title", "Modified")
  .expand("Author")
  .filter("Status eq 'Active'")
  .orderBy("Modified", false)
  .top(50)();

Each clause earns its place: select trims payload, filter and orderBy push work server-side, top bounds the page. Verify field internal names before publishing — display names fail silently in queries.

Select only required fields

Retrieving complete item payloads when only Id, Title, and Status render wastes network, parsing, memory, and time on every load. The current PnPjs equivalent of $select is the select clause above — project every query to the columns the UI actually renders, and treat untrimmed queries as defects in review.

BAD:      Retrieve complete item payload (all fields, all items)
BETTER:   .select("Id", "Title", "Status") + filter + page

Filtering

Filter on indexed, filterable columns — status equality, modified dates, booleans, numbers — using OData filter strings evaluated server-side. Server-side filtering scales; loading everything and filtering in React collapses past trivial volumes. Confirm filterable columns per list rather than assuming every column type supports every operator.

const active = await sp.web.lists
  .getByTitle("Projects")
  .items.select("Id", "Title", "Status")
  .filter("Status eq 'Active'")();

Ordering

Order server-side with the current syntax — Modified descending for activity views, Title ascending for directories — keeping ordering inside the paged query so pages stay stable across fetches:

const recent = await sp.web.lists
  .getByTitle("Projects")
  .items.select("Id", "Title", "Modified")
  .orderBy("Modified", false)
  .top(25)();

Top and limiting results

Limiting returned records bounds individual responses and keeps initial renders fast. Never present top as pagination replacement — a top-50 query answers "first fifty," not "all data in fifties," and treating it as paging silently drops everything beyond the limit.

Select and expand

Person and lookup columns arrive as references until expanded. Expand carefully — each expansion multiplies payload — and retrieve only the nested properties the UI renders:

const items = await sp.web.lists
  .getByTitle("Projects")
  .items.select("Id", "Title", "Author/Title", "Author/EMail")
  .expand("Author")();

Verify person and lookup internal names per list; avoid large nested expansions that reintroduce the payload problem select just solved.

Lookup fields

Reads resolve through lookup ID with expanded values; writes target the lookup ID. Where list relationships differ between environments — and they do after migrations — the lookup mapping belongs in configuration and validation, not in assumptions. Keep this section development-focused: IDs in, values out, mappings verified per environment.

Person fields

Retrieve Id with title or display value and email where available and appropriate — never assume every identity property is populated for every principal. Person data carries privacy weight: render only what the experience needs, respect that disabled or external principals resolve thinly, and keep permission-gated visibility in mind when caching or logging.

Choice and multi-choice fields

Reads return values directly; writes send the value or value collection the field type expects. Verify update payload syntax against current PnPjs and SharePoint behavior before publishing — choice serialization varies by single versus multi configuration, and guessing produces silent data corruption rather than loud errors.

Managed metadata

Taxonomy-backed fields can require special handling across PnPjs and SharePoint APIs — term identities, term store context, and validation behavior differ from plain columns. This guide covers the concept only: if taxonomy writes are in scope, verify current handling against official documentation first, and track a dedicated taxonomy deep-dive as future content rather than inventing a generic update example here.

Create list item

Creates flow from React form through validation, service, and PnPjs into the list — with async states, success confirmation, and failure messaging. Client-side validation improves UX; it never authorizes anything:

import type { SPFI } from "@pnp/sp";

export async function createProject(
  sp: SPFI,
  listTitle: string,
  project: INewProject
): Promise<number> {
  if (!project.title || !project.title.trim()) {
    throw new Error("A project title is required.");
  }
  const result = await sp.web.lists.getByTitle(listTitle).items.add({
    Title: project.title.trim(),
    Status: project.status
  });
  return result.data.Id as number;
}
  1. React form — validates for UX, submits a model.
  2. Service — validates again, calls PnPjs.
  3. PnPjs — adds the item under user permissions.
  4. SharePoint list — enforces required fields and policy.

Update list item

Send only changed, necessary fields — never round-trip entire items on every edit. Read-modify-write belongs in the service, concurrency expectations stay explicit, and failures surface which fields rejected the update rather than a generic fault:

import type { SPFI } from "@pnp/sp";

export async function setProjectStatus(
  sp: SPFI,
  listTitle: string,
  id: number,
  status: string
): Promise<void> {
  await sp.web.lists
    .getByTitle(listTitle)
    .items.getById(id)
    .update({ Status: status });
}

Delete list item

Deletes stay rare, confirmed, and permission-checked: confirmation UX first, service call second, error handling for already-deleted and denied cases. Destructive actions earn explicit confirmation dialogs and clear recovery messaging — never a bare icon button with no guard.

import type { SPFI } from "@pnp/sp";

export async function deleteProject(
  sp: SPFI,
  listTitle: string,
  id: number
): Promise<void> {
  await sp.web.lists
    .getByTitle(listTitle)
    .items.getById(id)
    .delete();
}

CRUD architecture

CREATE · READ · UPDATE · DELETE
↓
SERVICE (validation, mapping, errors)
↓
PnPjs
↓
SHAREPOINT

Surrounded by: Validation · Permissions ·
Error Handling · Logging

Files and document libraries

Libraries combine files with list-item metadata, so file work is always two-sided. Common operations — get files, upload, download and access, metadata updates, move and copy where supported, delete, folder navigation — each resolve through file and folder APIs verified below. Verify current file APIs against official PnP documentation before implementing upload and move paths.

File upload

Uploads resolve a target folder, send bytes with metadata handled separately, and confirm the result — with small versus large file paths differing only where current APIs materially diverge. Never invent size thresholds: verify current authoritative documentation if limits matter to the design, and test uploads at production sizes rather than with kilobyte samples:

import type { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/folders";
import "@pnp/sp/files";

export async function uploadStatusReport(
  sp: SPFI,
  libraryTitle: string,
  fileName: string,
  content: ArrayBuffer
): Promise<void> {
  const folder = sp.web.lists
    .getByTitle(libraryTitle)
    .rootFolder;
  await folder.files.addUsingPath(fileName, content, {
    Overwrite: true
  });
}

File metadata

A document library row is a file plus its associated list item — updates target the item while bytes target the file. Confusing the two produces metadata writes that vanish and file operations that ignore columns:

FILE → bytes, versions, check-out
ASSOCIATED LIST ITEM → columns, content types, approvals
METADATA UPDATE → item update after upload, verified by re-read

Folders

Get folders, list their files, create folders where the design requires, and navigate structures deliberately — without promoting deep nesting as default information architecture. Folder APIs serve migration-era structures and legitimate working sets; new designs still prefer flat libraries with metadata.

const files = await sp.web
  .getFolderByServerRelativePath(folderUrl)
  .files.select("Name", "Length")();

Users

Read the current user, enumerate site users, and resolve identities where appropriate — distinguishing SharePoint site users from Microsoft Entra users and Microsoft Graph users at every step. Identity properties vary by principal type and privacy posture, so verify availability per scenario rather than assuming every field resolves:

const me = await sp.web.currentUser.select("Title", "Email")();

SharePoint Groups

Get groups, read members, and check group information through the SharePoint group APIs; manage membership only where the acting user holds rights, since write operations enforce real authorization. Never confuse SharePoint Groups with Microsoft 365 Groups or Entra groups — different authorities, different APIs, different lifecycles:

const members = await sp.web.siteGroups
  .getByName(groupName)
  .users.select("Title", "Email")();

Microsoft 365 and Entra groups

These are Graph-oriented identity resources, not SharePoint constructs:

SharePoint Group → SharePoint API / PnPjs SharePoint
Microsoft 365 Group → Microsoft Graph
Entra Group → Microsoft Graph

Depth for the Graph side belongs in the future dedicated Graph article; this guide crosses the boundary only to keep the authorities straight.

Query text, selected properties, row limits, refiners where supported, paging, and result mapping through the current search APIs — without attempting the entire SharePoint Search schema here. Verify current search syntax against official PnP documentation; search shapes move more than list shapes:

import "@pnp/sp/search";

const results = await sp.search({
  Querytext: "project status",
  SelectProperties: ["Title", "Path"],
  RowLimit: 25
});

Search versus list query

List querySearch
Known list or library, structured filtering, item-centric operationsCross-site discovery over the search index, broader content finding

Neither is universally better: queries own known structures with freshness guarantees, search owns discovery across estates with index latency. Choose per question asked.

Paging

Fetching all items is the wrong production pattern for large datasets. Request one page, process and render it, fetch the next on demand, and continue — using the current paged pattern, verified per PnPjs release:

let page = await sp.web.lists
  .getByTitle("Projects")
  .items.select("Id", "Title")
  .top(100)
  .getPaged();

const firstPage = page.results;
if (page.hasNext) {
  page = await page.getNext();
}

Components only ever see the current page; the service owns continuation. Never invent page-size limits — page by API support and UI need, and verify the paged shape against current documentation since paging APIs evolve.

Large lists

Design around scalable queries from the start: indexed and filterable query design where applicable, required fields only, server-side filters, paging, search for discovery, and a data architecture that never loads entire large lists into browser memory. Verify exact SharePoint threshold claims before stating numbers — this guide states none, because behavior depends on indexes, views, and query shape, not on folklore.

Batching

Batching groups independent operations into fewer round trips through the current batched pattern — verified against official PnP documentation. Batch when operations are independent and round trips dominate; never batch by default, and never batch dependent sequences that need intermediate results:

const [batchedSP, execute] = sp.batched();

batchedSP.web.lists
  .getByTitle("Projects")
  .items.getById(1)
  .update({ Status: "Active" })
  .then(() => {
    // per-operation success handling
  })
  .catch(() => {
    // per-operation failure handling
  });

batchedSP.web.lists
  .getByTitle("Projects")
  .items.getById(2)
  .update({ Status: "Active" })
  .catch(() => {
    // failures stay attributable per operation
  });

// Executes the batched calls together
await execute();

Errors stay per-operation inside batches — aggregate blindly and one bad item poisons the diagnosis. Confirm batching semantics for the operations used; not every operation batches.

Batching versus parallel versus sequential

Batches suit independent operations the API supports together; parallel requests suit independent calls needing separate handling; sequential suits dependent operations where each step needs the previous result. Never fire uncontrolled parallel request sets — bound concurrency, handle per-request errors, and redesign when volume suggests paging or batching instead.

Async and await

Use async and await consistently with try and catch where errors matter — one promise style per service, no mixed idioms without reason. Every awaited PnPjs call can reject, so every service method either handles or deliberately propagates with context.

Error handling

Requests succeed into mapped data or fail into classification: PnPjs request, success maps data; failure classifies, logs, and returns or throws a useful application error that React renders as state. Cover 401, 403, 404, throttling, server errors, network failures, invalid queries, and invalid configuration — and never expose sensitive raw errors to users.

401 in PnPjs

Authentication, token, context, or target problems — never one universal cause. Investigate authentication context, the addressed API target, token acquisition, and external API configuration where applicable before touching query logic.

403 in PnPjs

Almost always authorization: current user permissions, site, list, or library access, operations needing greater permission, API permission issues where Graph or custom APIs participate, wrong target or resource, or tenant configuration. PnPjs never bypasses SharePoint permissions — a 403 is the platform working, and the fix is access or scope, not retry force.

404 in PnPjs

Incorrect URLs, renamed or misreferenced lists, wrong site context, file or folder paths, or environment configuration. Hard-coded URLs make 404s an environment-migration specialty — externalize identifiers and re-verify per environment.

429 and throttling

Too many requests in too short a window trigger throttling: respect server guidance such as Retry-After where provided, reduce unnecessary calls, batch and page appropriately, and cache where appropriate. Never claim the library retries automatically without verifying current PnPjs retry behavior, and never invent retry counts or delays — back off, spread load, and design request volume down first.

Retry strategy

Retry transient errors only — network blips and throttled windows with server guidance. Never blindly retry permission denials, invalid queries, or missing resources without correcting the cause; exponential backoff applies only where consistent with current Microsoft and PnP guidance, and every retry path needs a visible ceiling.

Performance

Select, filter, page, and map — then render only needed data. Review payload size, request count, round trips, batching, caching, duplicate requests, React integration, and large-list behavior as one system: the fastest component cannot outrun a service that fetches everything to render three fields.

BAD:      Get everything → Filter in React → Render everything
BETTER:   Select + Filter + Page + Map → Render needed data

Avoid N+1 requests

Fetching a hundred items then firing one request per item creates a hundred-and-one-request page. Prefer expand, batching, preloaded reference data, or redesigned queries where supported — while admitting expand is not always appropriate and reference data must stay fresh enough for the scenario.

Caching

Cache reference data, configuration, and slow-changing values — weighing staleness, user-specific scoping, permission sensitivity, and invalidation on every entry. Never cache sensitive or privileged data indiscriminately; a stale public lookup is a bug, a leaked privileged value is an incident.

Request deduplication

Multiple components requesting identical data multiply traffic silently. Centralize through a shared service with a request and cache strategy so concurrent subscribers share one flight. Keep this conceptual unless demonstrating accurately against current PnPjs caching APIs — a hand-rolled broken cache costs more than duplicate requests.

PnPjs with React hooks

Components consume hooks, hooks consume services, services call PnPjs into SharePoint — the exact layering from the React guide, repeated here because data features live or die by it. One concise shape; deeper hooks and component architecture live in SPFx and React:

React → useProjects → ProjectService → PnPjs → SharePoint

Security checklist

Standing takeaway: PnPjs makes API calls easier; it never replaces authorization.

Input validation

Validate form and component data before use — types, ranges, required values — while remembering client-side validation improves UX without forming any security boundary. Server and platform authorization still decide every write.

Unsafe content and XSS

List data rendered as HTML crosses a trust boundary: discuss sanitization, restrict allowed markup, and document why any raw rendering exists. Never encourage raw HTML rendering casually, and publish no insecure examples — text rendering and safe components are the default.

Configuration

Externalize tenant URLs, site URLs, list IDs, library paths, and API endpoints into environment configuration consumed by services — never inline constants. Weigh list ID stability against title readability per environment, and never treat browser-visible configuration as secret storage.

  1. Environment — per-stage values owned explicitly.
  2. Configuration — resolved at runtime or build per stage.
  3. Service — consumes configuration, never constants.
  4. PnPjs — called with resolved values.

Multi-site solutions

Solutions reading across sites need target web and site context, cross-site permissions, reusable SPFI scoping, and configuration per target. The documented different-web pattern scopes instances per target — verify current multi-site patterns against official PnP documentation before publishing cross-site code, and test permissions in every addressed site, not just the host.

Cross-tenant calls

Normal PnPjs SharePoint calls do not cross tenant boundaries automatically. Cross-tenant scenarios add identity, authentication, permissions, tenant configuration, and API architecture concerns that each need explicit design — keep this section as the warning it is, and route real cross-tenant needs to dedicated identity and API design, not to query tweaks.

Testing services

Test successful, empty, permission-denied, missing-list, misconfigured, large or paged, and write-failure responses through mock service and API boundaries. Services behind interfaces test without tenants; components behind hooks test without services.

Mock service

React → IProjectService
Production:  PnPProjectService → SharePoint
Testing:     MockProjectService → Static Data

Separation of concerns paying off twice: production swaps data sources without touching UI, and tests run offline against predictable fixtures.

Production example: project portfolio

One illustrative architecture threading this guide — never a customer solution. A project portfolio web part reads a Projects list (Title, Status, Owner, StartDate, Modified) through a useProjects hook into a project service into PnPjs, offering status filtering, paging, project creation and updates, error handling, and refresh:

  1. SPFx — context and service construction.
  2. React — filter, list, paging, and form components.
  3. useProjects — data, loading, error, refresh.
  4. IProjectService — the contract everything depends on.
  5. PnPProjectService — PnPjs implementation.
  6. PnPjs — selected, filtered, paged SharePoint calls.
  7. SharePoint Projects list — data behind user permissions.

Initialization, read, create, update, delete, paging, batching

Every example above follows current, verified patterns: SPFI bound to web part context, selective imports, field projection, server-side filtering, paged reads, attributed writes, and batched independence. Verify each against official PnP documentation at implementation time — patterns evolve, and this guide's shapes track the documented current generation.

PnPjs versus raw REST

Same goal — active projects — two expressions. Raw REST constructs the endpoint, query string, HTTP request, and response parsing by hand; PnPjs expresses the identical query fluently. Neither is made to look bad here: REST knowledge remains valuable for debugging, unsupported operations, and understanding what PnPjs sends on your behalf.

RAW REST:  endpoint + query string → HTTP request → parse
PNPJS:     sp.web.lists… .select… .filter… .orderBy… .top… .getPaged()

PnPjs and Microsoft Graph

PnPjs also reaches Microsoft Graph through its graph packages and parallel factory — useful where SharePoint-adjacent identity and group data rides along. For advanced Graph integration, see the dedicated SPFx and Microsoft Graph guide; this guide crosses over only to keep the authorities straight and the imports aliased correctly.

PnPjs in migration and modernization

Legacy JavaScript built on raw REST or JSOM modernizes operation by operation into service-layer PnPjs, Graph, or REST inside SPFx — never wholesale rewrites into PnPjs by default. Depth: Script Editor to SPFx modernization and Classic to Modern SharePoint.

  1. Legacy JavaScript — raw REST or JSOM as found.
  2. Modernization — requirement-first routing per script.
  3. SPFx — service layer behind interfaces.
  4. PnPjs, Graph, or REST — chosen per operation.

Architecture blueprint: SPFx and PnPjs

Labeled solution blueprint, not a customer implementation: SharePoint page hosting an SPFx web part, React behind a custom hook, service interface into a PnPjs service through SPFI, reaching lists, libraries, files, users, groups, and search — surrounded by paging, filtering, error handling, permissions, caching, logging, and configuration discipline.

  1. SharePoint page — host surface.
  2. SPFx web part — context and properties.
  3. React — components and hook.
  4. Custom hook — data, loading, error, refresh.
  5. Service interface — the contract.
  6. PnPjs service — verified current patterns.
  7. SPFI — configured instance per service.
  8. SharePoint — lists, libraries, files, users, groups, search.

Production readiness checklist

Setup: current PnPjs version verified; current initialization used; only required packages installed.

Architecture: service layer; interfaces and models; React separated from data logic.

Queries: required fields selected; filters server-side where appropriate; paging implemented where required; N+1 patterns reviewed.

Files: file operations validated; metadata handling tested.

Security: user permissions tested; no secrets; write authorization tested.

Reliability: errors handled; 403 tested; 404 tested; throttling considered; retry behavior understood.

Performance: request count reviewed; payload reviewed; caching considered; large datasets tested.

Production: environment configuration validated; multiple user personas tested; logging reviewed; ownership documented.

Building a data-heavy SPFx solution

If you are working through PnPjs architecture, large SharePoint datasets, permissions, performance, Microsoft Graph, or complex enterprise integrations, share the technical challenge with nextM365: Discuss Your SPFx Project.

Continue with Explore SPFx, the SPFx complete guide, SPFx and React, Script Editor to SPFx modernization, and Microsoft Graph for beginners.

Related resources

Share this:

Topics covered

React · Project Structure · Permissions · ALM · Security

Frequently asked questions

What is PnPjs?

An open-source, TypeScript-friendly JavaScript library with a fluent API over SharePoint and parts of Microsoft Graph. It simplifies API calls but creates no new platform permissions and bypasses nothing — authorization still comes from SharePoint and Entra ID.

Can PnPjs be used with SPFx?

Yes — it is the standard data-access companion for SPFx. Install the needed @pnp packages, initialize an SPFI instance with the web part context, import only the modules used, and call PnPjs from a service layer rather than from components.

How do I initialize PnPjs in SPFx?

Create an SPFI instance with spfi().using(SPFx(context)) — typically once per service, constructed with the web part context — and reuse that instance. Avoid creating SPFI inside components or event handlers.

Is PnPjs better than SharePoint REST?

It is more convenient, not more capable: PnPjs calls the same underlying APIs with typed, fluent syntax. REST knowledge stays valuable for debugging, unsupported operations, and understanding what PnPjs actually sends.

Can PnPjs use Microsoft Graph?

PnPjs ships Graph capabilities through its graph packages with a parallel graphfi factory. For deep Graph work, prefer the dedicated Graph patterns — and a future dedicated article — over stretching SharePoint-shaped code.

How do I get SharePoint list items with PnPjs?

Reference the list by title or ID, then select only required fields, filter server-side, order, and page: sp.web.lists.getByTitle(title).items.select(...).filter(...).orderBy(...).top(...).getPaged() — always behind a service that maps results to application models.

How do I filter items with PnPjs?

With OData filter strings on indexed, filterable columns — status, dates, booleans, numbers — evaluated server-side. Never load entire lists to filter in React when the API can filter first.

How do I paginate PnPjs results?

Use the current paged pattern that returns results with a continuation, rendering one page at a time and fetching the next on demand. Paging belongs in the service layer so components only ever see the current page.

Does PnPjs support batching?

Yes — group independent operations with the current batched pattern and execute them together, handling per-operation errors. Batch for fewer round trips, not as a default; dependent or heavy operations often belong sequential or redesigned.

Can PnPjs upload files?

Yes, through the file and folder APIs with metadata handled on the associated list item. Verify current upload APIs for small versus large files against official PnP documentation before implementing.

Can PnPjs manage SharePoint Groups?

It can read groups, membership, and site users, and manage membership where the acting user has rights. Never confuse SharePoint Groups with Microsoft 365 or Entra groups, which are Graph-oriented identity resources.

Why does PnPjs return 403?

Almost always authorization, not a library defect: the current user lacks rights on the target, the operation needs greater permission, the wrong resource was addressed, or a Graph call lacks an approved scope. Verify effective access first.

Does PnPjs bypass SharePoint permissions?

No — never. Every call executes under real user and platform authorization. Code that "works for me" fails for restricted users, which is why persona testing is part of every PnPjs feature.

Sources

Have a Microsoft 365 topic idea?

Share article suggestions, community session ideas, corrections, or real-world scenarios for future nextM365 learning notes.

Connect with me

Keep learning Microsoft 365

Explore more practical tutorials for SharePoint, Power Platform, Copilot Studio, migration, automation, governance, and security.

Continue learning