- Published on
Calling Microsoft Graph from SPFx — The Right Way
Most SPFx Graph examples show you the bare minimum — get a client, make a call, log the result.
What they skip is everything that matters in production: typed responses, error handling, retry on transient failures, and a structure that does not scatter Graph calls across every component in your solution.
This article shows you how to call Graph the right way from SPFx, and how to wrap it in a reusable service layer your entire web part can share.
📦 No Extra Packages Required
SPFx ships with built-in Graph support via MSGraphClientFactory. You do not need to install the Graph SDK separately — the factory is available directly from this.context.
# No npm install needed — MSGraphClientFactory is built into @microsoft/sp-http
⚙️ Getting the Graph Client
The correct client to use in SPFx 1.14+ is MSGraphClientV3. It wraps the Microsoft Graph JavaScript SDK v3 and handles AAD token acquisition automatically.
import { MSGraphClientV3 } from '@microsoft/sp-http';
const client: MSGraphClientV3 = await this.context.msGraphClientFactory.getClient('3');
The '3' argument tells the factory to return a v3 client. Always use v3 — the older v1/v2 clients are deprecated.
Call getClient inside onInit if you need the client for the lifetime of the web part, or inside an async function each time you need it. The factory caches the token internally — repeated calls to getClient are cheap.
🧩 Building a Typed Graph Service
Scattering await this.context.msGraphClientFactory.getClient('3') across every component is a maintenance problem. Wrap Graph calls in a dedicated service class instead.
src/services/GraphService.ts
import { MSGraphClientV3 } from '@microsoft/sp-http';
export interface IUserProfile {
id: string;
displayName: string;
mail: string;
jobTitle: string;
department: string;
}
export interface IGraphServiceResult<T> {
data: T | null;
error: string | null;
}
export class GraphService {
private _client: MSGraphClientV3;
constructor(client: MSGraphClientV3) {
this._client = client;
}
// Get the current user's profile
public async getCurrentUser(): Promise<IGraphServiceResult<IUserProfile>> {
try {
const response = await this._client
.api('/me')
.select('id,displayName,mail,jobTitle,department')
.get();
return { data: response as IUserProfile, error: null };
} catch (err) {
return { data: null, error: this._extractErrorMessage(err) };
}
}
// Get members of a group by ID
public async getGroupMembers(groupId: string): Promise<IGraphServiceResult<IUserProfile[]>> {
try {
const response = await this._client
.api(`/groups/${groupId}/members`)
.select('id,displayName,mail,jobTitle,department')
.get();
return { data: response.value as IUserProfile[], error: null };
} catch (err) {
return { data: null, error: this._extractErrorMessage(err) };
}
}
// Extract a readable error message from Graph error responses
private _extractErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'object' && err !== null) {
const graphErr = err as { body?: string; statusCode?: number };
if (graphErr.body) {
try {
const parsed = JSON.parse(graphErr.body);
return parsed?.error?.message ?? 'Unknown Graph error';
} catch {
return graphErr.body;
}
}
}
return 'Unknown error';
}
}
🔧 Initialising the Service in onInit
Initialise the service once in onInit and store it on the web part class. Pass it to your React component as a prop.
import { MSGraphClientV3 } from '@microsoft/sp-http';
import { GraphService } from './services/GraphService';
export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
private _graphService: GraphService;
protected async onInit(): Promise<void> {
await super.onInit();
const client: MSGraphClientV3 = await this.context.msGraphClientFactory.getClient('3');
this._graphService = new GraphService(client);
}
public render(): void {
const element = React.createElement(MyComponent, {
graphService: this._graphService
});
ReactDOM.render(element, this.domElement);
}
}
⚛️ Consuming the Service in a React Component
import * as React from 'react';
import { GraphService, IUserProfile } from '../../services/GraphService';
import { Spinner } from '@fluentui/react-components';
interface IMyComponentProps {
graphService: GraphService;
}
const MyComponent: React.FC<IMyComponentProps> = ({ graphService }) => {
const [user, setUser] = React.useState<IUserProfile | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
graphService.getCurrentUser().then(({ data, error: err }) => {
if (err) {
setError(err);
} else {
setUser(data);
}
setLoading(false);
});
}, []);
if (loading) return <Spinner label="Loading profile..." />;
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>;
if (!user) return null;
return (
<div>
<h3>{user.displayName}</h3>
<p>{user.jobTitle} — {user.department}</p>
<p>{user.mail}</p>
</div>
);
};
export default MyComponent;
🔁 Adding Retry for Transient Failures
Graph API calls can transiently fail with 429 (throttled) or 503 (service unavailable). A simple retry wrapper handles both:
private async _withRetry<T>(
fn: () => Promise<T>,
retries: number = 3,
delayMs: number = 1000
): Promise<T> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
const status = (err as { statusCode?: number }).statusCode;
const isRetryable = status === 429 || status === 503;
if (isRetryable && attempt < retries) {
// Respect Retry-After header if present, otherwise use exponential backoff
const retryAfter = (err as { headers?: { 'retry-after'?: string } })
.headers?.['retry-after'];
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : delayMs * attempt;
await new Promise(resolve => setTimeout(resolve, waitMs));
} else {
throw err;
}
}
}
throw new Error('Max retries exceeded');
}
Wrap your Graph calls with it:
public async getCurrentUser(): Promise<IGraphServiceResult<IUserProfile>> {
try {
const response = await this._withRetry(() =>
this._client.api('/me').select('id,displayName,mail,jobTitle,department').get()
);
return { data: response as IUserProfile, error: null };
} catch (err) {
return { data: null, error: this._extractErrorMessage(err) };
}
}
🔐 Declaring Graph Permissions
Graph calls will fail with 403 if the required permission scopes are not declared in your solution manifest.
In config/package-solution.json:
{
"solution": {
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read"
},
{
"resource": "Microsoft Graph",
"scope": "GroupMember.Read.All"
}
]
}
}
After deploying the .sppkg, a SharePoint admin must approve these permissions in the SharePoint Admin Center → Advanced → API access page. Permissions are tenant-wide — once approved, all SPFx solutions on the tenant can use them.
📂 GitHub Source
The companion repository contains the full GraphService class with retry logic, typed interfaces for common Graph endpoints, and a working web part that demonstrates the pattern end-to-end.
View full SPFx project on GitHub:SPFx Graph API service wrapper — typed client with error handling and retry
✅ Summary
- Use
MSGraphClientV3viathis.context.msGraphClientFactory.getClient('3')— the v1/v2 clients are deprecated. - Initialise the client once in
onInit, not insiderenderor component effects. - Wrap Graph calls in a typed service class — keep components free of direct API calls.
- Use a
IGraphServiceResult<T>return shape so every call site handles both success and error without try/catch duplication. - Add retry logic for 429 and 503 responses — Graph throttles under load and transient failures are common.
- Declare all required permission scopes in
package-solution.jsonand have them approved by an admin before testing in the hosted workbench.
Happy coding!
Author
Ravichandran@Hi_Ravichandran
