How to Build a Modern SPFx Web Part with Microsoft Dataverse CRUD Operations

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.

SPFx Dataverse Architecture Diagram

Key Components

  1. AadHttpClient / Entra ID Authorization: Configured in config/package-solution.json to request permissions for your Dataverse environment URL (https://<your-org>.api.crm.dynamics.com).

  2. TypeScript Interfaces: Strongly typed models representing employee records, departments, and payload contracts.

  3. Service Layer Pattern: A dedicated DataverseService class isolating all HTTP endpoints from the React UI components.

  4. Fluent UI (React): Clean M365-native interface utilizing DetailsList for 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

  1. 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).

  2. 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.

  3. Graceful Permission Errors: Users who don't have security roles assigned in Dataverse will receive 403 Forbidden errors. Wrap service calls in clear try/catch blocks to show actionable Fluent UI MessageBar errors instead of failing silently.


Complete Repository & Source Code

You can clone, review, or fork the complete working solution from GitHub:

GitHub Repositoryhttps://github.com/RamPrashanthPaladugu/spfx-dataverse-employee-crud/tree/master


No comments:

Post a Comment

How to Build a Modern SPFx Web Part with Microsoft Dataverse CRUD Operations

When building internal applications on Microsoft 365, SharePoint Lists are often the default storage layer. However, once you need relationa...