When building internal applications on Microsoft 365, SharePoint Lists are often the default storage layer. However, once you need relational data, granular column-level security, or direct integration with Dynamics 365 and Power Platform solutions, Microsoft Dataverse becomes the superior choice.
In this walkthrough, we’ll look at how to build an enterprise-grade SharePoint Framework (SPFx) web part that performs complete CRUD (Create, Read, Update, Delete) operations against Dataverse entities using Microsoft Graph API / Dataverse Web API and TypeScript.
Architecture Overview
Connecting SPFx to Dataverse requires seamless authentication without exposing credentials or managing complex OAuth token handshakes manually. SPFx provides native context objects (AadHttpClient / MSGraphClientV3) that leverage Microsoft Entra ID (Azure AD) implicit grant/PKCE flows under the hood.
Key Components
AadHttpClient/ Entra ID Authorization: Configured inconfig/package-solution.jsonto request permissions for your Dataverse environment URL (https://<your-org>.api.crm.dynamics.com).TypeScript Interfaces: Strongly typed models representing employee records, departments, and payload contracts.
Service Layer Pattern: A dedicated
DataverseServiceclass isolating all HTTP endpoints from the React UI components.Fluent UI (React): Clean M365-native interface utilizing
DetailsListfor tabular data, modal forms for edits, and primary action buttons.
Step-by-Step Implementation
1. Requesting Dataverse Permissions in SPFx
Before writing code, your SPFx package needs permission to call the Dataverse REST endpoint. Add the permission request to config/package-solution.json:
"webApiPermissionRequests": [
{"resource": "https://your-org-name.crm.dynamics.com","scope": "user_impersonation"}]
Note: Once deployed to the SharePoint App Catalog, tenant admins must approve this request under the SharePoint Admin Center > API access page.
2. Defining the Employee Data Model
Creating strict TypeScript interfaces ensures type safety across form validation and REST call serialization:
// models/IEmployee.ts
export interface IEmployee {
cr01_employeeid?: string; // Dataverse Primary Key (GUID)
cr01_firstname: string; // First Name
cr01_lastname: string; // Last Name
cr01_email: string; // Email Address
cr01_department: string; // Department
cr01_jobtitle: string; // Job Title
cr01_joiningdate?: string; // ISO Date String
}
3. Creating the Dataverse Service Layer
By wrapping AadHttpClient calls inside a modular service, we keep React UI code clean and readable.
// services/DataverseService.ts
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { IEmployee } from '../models/IEmployee';
export class DataverseService {
private client: AadHttpClient;
private baseUrl: string = "https://your-org-name.api.crm.dynamics.com/api/data/v9.2/";
constructor(context: WebPartContext, serviceUri: string) {
this.baseUrl = `${serviceUri}/api/data/v9.2/`;
}
public async init(context: WebPartContext, serviceUri: string): Promise<void> {
this.client = await context.aadHttpClientFactory.getClient(serviceUri);
}
// 1. READ: Fetch all employees
public async getEmployees(): Promise<IEmployee[]> {
const endpoint = `${this.baseUrl}cr01_employees?$select=cr01_employeeid,cr01_firstname,cr01_lastname,cr01_email,cr01_department,cr01_jobtitle`;
const response: HttpClientResponse = await this.client.get(
endpoint,
AadHttpClient.configurations.v1
);
const data = await response.json();
return data.value;
}
// 2. CREATE: Add new employee
public async createEmployee(employee: IEmployee): Promise<void> {
const endpoint = `${this.baseUrl}cr01_employees`;
await this.client.post(
endpoint,
AadHttpClient.configurations.v1,
{
headers: {
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
},
body: JSON.stringify(employee)
}
);
}
// 3. UPDATE: Modify existing record using PATCH
public async updateEmployee(employeeId: string, employee: Partial<IEmployee>): Promise<void> {
const endpoint = `${this.baseUrl}cr01_employees(${employeeId})`;
await this.client.fetch(
endpoint,
AadHttpClient.configurations.v1,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
'If-Match': '*' // Overwrite concurrency check
},
body: JSON.stringify(employee)
}
);
}
// 4. DELETE: Remove employee by GUID
public async deleteEmployee(employeeId: string): Promise<void> {
const endpoint = `${this.baseUrl}cr01_employees(${employeeId})`;
await this.client.fetch(
endpoint,
AadHttpClient.configurations.v1,
{
method: 'DELETE',
headers: {
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
}
}
);
}
}
4. Wire Up to the React UI Component
In the React layer, initialize DataverseService inside componentDidMount (or useEffect for functional components), manage form states for creation/editing, and trigger UI refreshes after operations complete.
// components/EmployeeCrud.tsx
import * as React from 'react';
import { DetailsList, SelectionMode, PrimaryButton, DefaultButton, Dialog } from '@fluentui/react';
import { IEmployee } from '../models/IEmployee';
import { DataverseService } from '../services/DataverseService';
export const EmployeeCrud: React.FC<IEmployeeProps> = (props) => {
const [employees, setEmployees] = React.useState<IEmployee[]>([]);
const [loading, setLoading] = React.useState<boolean>(true);
React.useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
const service = new DataverseService(props.context, props.environmentUrl);
await service.init(props.context, props.environmentUrl);
const data = await service.getEmployees();
setEmployees(data);
setLoading(false);
};
return (
<div>
<h2>Employee Management (Dataverse)</h2>
<PrimaryButton text="Add Employee" onClick={() => /* Open Modal */ {}} />
<DetailsList
items={employees}
selectionMode={SelectionMode.single}
columns={[
{ key: 'col1', name: 'First Name', fieldName: 'cr01_firstname', minWidth: 100 },
{ key: 'col2', name: 'Last Name', fieldName: 'cr01_lastname', minWidth: 100 },
{ key: 'col3', name: 'Email', fieldName: 'cr01_email', minWidth: 150 },
{ key: 'col4', name: 'Department', fieldName: 'cr01_department', minWidth: 120 },
]}
/>
</div>
);
};
Best Practices & Lessons Learned
Always Use Singular/Plural Naming in OData Correctly: Dataverse entities use plural schema names in API endpoints (e.g., entity is
cr01_employee, but the Web API route is/cr01_employees).Handle Lookup & Choice Fields properly: If your Dataverse table uses Choice columns or Foreign Key lookup columns, use OData bind syntax (e.g.,
"cr01_Department@odata.bind": "/cr01_departments(guid)") for associations.Graceful Permission Errors: Users who don't have security roles assigned in Dataverse will receive
403 Forbiddenerrors. Wrap service calls in cleartry/catchblocks to show actionable Fluent UIMessageBarerrors instead of failing silently.
Complete Repository & Source Code
GitHub Repository: https://github.com/RamPrashanthPaladugu/spfx-dataverse-employee-crud/tree/master

No comments:
Post a Comment