Make SharePoint Online Batch API Request Easy In Single Line Of Code

Introduction

 
In this article, we will learn about making SharePoint Online Batch Request Call with a few lines of code using Batch Utils which I have created.
 

Description

 
For easy SharePoint List Item CRUD Operations, I have created an SPRest utility. You can find a detailed article on how to use SPRest in development over here
 
If you are working with SharePoint Online, then you definitely have used SharePoint REST API Call to get data with multiple requests from the same site collection.
 
So, as per the client requirement, you may have to request 5, 10, 15, or any number of AJAX requests to fulfill the requirement. By doing so, you may be stuck with a performance issue for a large number of requests.
 
If you are using SharePoint Online, SP 2016, or SP 2019 then there is good news. Microsoft has introduced OData $batch API support for REST Call.
 
Using $batch REST API Call, you can make up to 100 HTTP Requests with Single Batch Request. 
 
There are limited articles and blogs which describe how to use $batch API in SharePoint Online. I have found difficulty in configuring or writing $batch REST Call.
 
For developers, I have created this SharePoint Batch Utility which is easy to integrate and easy to use for the Get operation. The code is properly exaplined via commented lines.
 
BatchUtils.ts 
  1. //Reference from  Vardhman Despande blogs https://www.vrdmn.com/2016/06/sharepoint-online-get-userprofile.html  
  2. //Reference from  https://github.com/andrewconnell/sp-o365-rest/blob/master/SpRestBatchSample/Scripts/App.js  
  3. //var arr =["https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(212)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(213)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(214)"]  
  4. var BatchUtils = (() => {  
  5.     /** 
  6.        * Build the batch request for each property individually 
  7.        * @param endPointsToGet  
  8.        * @param boundryString  
  9.        */  
  10.     function buildBatchRequestBody(endPointsToGet, boundryString) {  
  11.         var propData = new Array();  
  12.         for (var i = 0; i < endPointsToGet.length; i++) {  
  13.             var getPropRESTUrl = endPointsToGet[i];  
  14.             propData.push('--batch_' + boundryString);  
  15.             propData.push('Content-Type: application/http');  
  16.             propData.push('Content-Transfer-Encoding: binary');  
  17.             propData.push('');  
  18.             propData.push('GET ' + getPropRESTUrl + ' HTTP/1.1');  
  19.             propData.push('Accept: application/json;odata=verbose');  
  20.             propData.push('');  
  21.         }  
  22.         return propData.join('\r\n');  
  23.     }  
  24.     function BuildChangeSetRequestBody(changeSetId, action, endpoint, items) {  
  25.         var batchContents = [];  
  26.         let item;  
  27.         // create the changeset  
  28.         for (let i = 0; i < items.length; i++) {  
  29.             item = items[i].data;  
  30.             //action=items[i].action;  
  31.             //TODO -Need to test for different action  
  32.             endpoint = items[i].reqUrl;  
  33.             batchContents.push("--changeset_" + changeSetId);  
  34.             batchContents.push("Content-Type: application/http");  
  35.             batchContents.push("Content-Transfer-Encoding: binary");  
  36.             batchContents.push("");  
  37.             if (action === "UPDATE") {  
  38.                 batchContents.push("PATCH " + endpoint + " HTTP/1.1");  
  39.                 batchContents.push("If-Match: *");  
  40.                 batchContents.push("Content-Type: application/json;odata=verbose");  
  41.                 batchContents.push("");  
  42.                 batchContents.push(JSON.stringify(item));  
  43.             } else if (action === "ADD") {  
  44.                 batchContents.push("POST " + endpoint + " HTTP/1.1");  
  45.                 //For Insert it will return created value in json format  
  46.                 batchContents.push('Accept: application/json;odata=verbose');  
  47.                 batchContents.push("Content-Type: application/json;odata=verbose");  
  48.                 batchContents.push("");  
  49.                 batchContents.push(JSON.stringify(item));  
  50.             } else if (action === "DELETE") {  
  51.                 batchContents.push("DELETE " + endpoint + " HTTP/1.1");  
  52.                 batchContents.push("If-Match: *");  
  53.             }  
  54.             //Commented POST request line and added code for UPDATE as well  
  55.             // batchContents.push("POST " + endpoint + " HTTP/1.1");  
  56.             // batchContents.push("Content-Type: application/json;odata=verbose");  
  57.             // batchContents.push("");  
  58.             // batchContents.push(JSON.stringify(_item));  
  59.             batchContents.push("");  
  60.   
  61.             // // END changeset to create data  
  62.             // batchContents.push("--changeset_" + changeSetId + "--");  
  63.   
  64.         }  
  65.          // END changeset to create data  
  66.             batchContents.push("--changeset_" + changeSetId + "--");  
  67.   
  68.         // batch body  
  69.         return batchContents.join("\r\n");  
  70.   
  71.     }  
  72.     let BuildChangeSetRequestHeader = (batchGuid, changeSetId, batchBody) => {  
  73.         let batchContents = [];  
  74.   
  75.         // create batch for creating items  
  76.         batchContents.push("--batch_" + batchGuid);  
  77.         batchContents.push(  
  78.             'Content-Type: multipart/mixed; boundary="changeset_' +  
  79.             changeSetId +  
  80.             '"'  
  81.         );  
  82.         batchContents.push("Content-Length: " + batchBody.length);  
  83.         batchContents.push("Content-Transfer-Encoding: binary");  
  84.         batchContents.push("");  
  85.         batchContents.push(batchBody);  
  86.         batchContents.push("");  
  87.   
  88.         // create request in batch to get all items after all are created  
  89.         ////Commented below endpoint as we are utilizing same endpoint without orderby  
  90.         // endpoint = _this.rootUrl +  
  91.         //     "/_api/web/lists/getbytitle('" + listName + "')" +  
  92.         //     '/items?$orderby=Title';  
  93.   
  94.         // batchContents.push('--batch_' + batchGuid);  
  95.         // batchContents.push('Content-Type: application/http');  
  96.         // batchContents.push('Content-Transfer-Encoding: binary');  
  97.         // batchContents.push('');  
  98.         //COmmented below lines of code as I don't need to request GET after insertion  
  99.         // batchContents.push('GET ' + endpoint + ' HTTP/1.1');  
  100.         // batchContents.push('Accept: application/json;odata=verbose');  
  101.         // batchContents.push('');  
  102.   
  103.         batchContents.push("--batch_" + batchGuid + "--");  
  104.   
  105.         return batchContents.join("\r\n");  
  106.     }  
  107.   
  108.     /** 
  109.      * Build the batch header containing the user profile data as the batch body 
  110.      * @param userPropsBatchBody  
  111.      * @param boundryString  
  112.      */  
  113.     function buildBatchRequestHeader(userPropsBatchBody, boundryString) {  
  114.         var headerData = [];  
  115.         headerData.push('Content-Type: multipart/mixed; boundary="batch__' + boundryString + '"');  
  116.         headerData.push('Content-Length: ' + userPropsBatchBody.length);  
  117.         headerData.push('Content-Transfer-Encoding: binary');  
  118.         headerData.push('');  
  119.         headerData.push(userPropsBatchBody);  
  120.         headerData.push('');  
  121.         headerData.push('--batch_' + boundryString + '--');  
  122.         return headerData.join('\r\n');  
  123.     }  
  124.   
  125.     /** 
  126.      * Parse Batch Get Response 
  127.      * @param batchResponse  
  128.      */  
  129.     function parseResponse(batchResponse) {  
  130.         //Extract the results back from the BatchResponse  
  131.         var results = grep(batchResponse.split("\r\n"), function (responseLine) {  
  132.             try {  
  133.                 return responseLine.indexOf("{") != -1 && typeof JSON.parse(responseLine) == "object";  
  134.             }  
  135.             catch (ex) { /*adding the try catch loop for edge cases where the line contains a { but is not a JSON object*/ }  
  136.         }, null);  
  137.   
  138.         //Convert JSON strings to JSON objects  
  139.         return results.map(function (result) {  
  140.             return JSON.parse(result);  
  141.         });  
  142.     }  
  143.     /** 
  144.      * Copied grep function from jQuery JavaScript Library v1.11.3 
  145.      * @param elems  
  146.      * @param callback  
  147.      * @param invert  
  148.      */  
  149.     var grep = function (elems, callback, invert) {  
  150.         var callbackInverse,  
  151.             matches = [],  
  152.             i = 0,  
  153.             length = elems.length,  
  154.             callbackExpect = !invert;  
  155.   
  156.         // Go through the array, only saving the items  
  157.         // that pass the validator function  
  158.         for (; i < length; i++) {  
  159.             callbackInverse = !callback(elems[i], i);  
  160.             if (callbackInverse !== callbackExpect) {  
  161.                 matches.push(elems[i]);  
  162.             }  
  163.         }  
  164.   
  165.         return matches;  
  166.     };  
  167.   
  168.     /** 
  169.      * Get Uniquie boundry string for batch request identifier 
  170.      */  
  171.     function getBoundryString() {  
  172.         return "vrd_" + Math.random().toString(36).substr(2, 9);  
  173.     }  
  174.   
  175.     var makeBatchRequests = ({ rootUrl, batchUrls, FormDigestValue }) => {  
  176.         if (FormDigestValue) {  
  177.             return internalBatch({ FormDigestValue: FormDigestValue, rootUrl, batchUrls })  
  178.         } else {  
  179.   
  180.             return fetch(`${rootUrl}/_api/contextinfo`, {  
  181.                 method: "POST",  
  182.                 "headers": { "Accept""application/json;odata=verbose", credentials: "include", }  
  183.             }).then(r => r.json()).then(r => {  
  184.                 return internalBatch({ FormDigestValue: r.d.GetContextWebInformation.FormDigestValue, rootUrl, batchUrls })  
  185.   
  186.             });  
  187.         }  
  188.     }  
  189.     var makePostBatchRequests = ({ rootUrl, batchUrls, FormDigestValue }) => {  
  190.         if (FormDigestValue) {  
  191.             return internalPostBatch({ FormDigestValue: FormDigestValue, rootUrl, batchUrls })  
  192.         } else {  
  193.   
  194.             return fetch(`${rootUrl}/_api/contextinfo`, {  
  195.                 method: "POST",  
  196.                 "headers": { "Accept""application/json;odata=verbose", credentials: "include", }  
  197.             }).then(r => r.json()).then(r => {  
  198.                 return internalPostBatch({ FormDigestValue: r.d.GetContextWebInformation.FormDigestValue, rootUrl, batchUrls })  
  199.   
  200.             });  
  201.         }  
  202.     }  
  203.     var internalPostBatch = ({ FormDigestValue, rootUrl, batchUrls }) => {  
  204.         //Reference from  Vardhman Despande blogs https://www.vrdmn.com/2016/06/sharepoint-online-get-userprofile.html  
  205.   
  206.         //AccountName of the user  
  207.         //  var userAccountName = encodeURIComponent("i:0#.f|membership|[email protected]");  
  208.   
  209.         //Collection of Endpoint url to fetch  
  210.         var endpointsArray = batchUrls;//["https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(212)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(213)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(214)"]  
  211.   
  212.         //Unique identifier, will be used to delimit different parts of the request body  
  213.         var boundryString = getBoundryString();  
  214.         //Unique identifier, will be used to delimit different parts of the request body  
  215.         var changeSetIdString = getBoundryString();  
  216.   
  217.         //Build Body of the Batch Request  
  218.         var userPropertiesBatchBody = BuildChangeSetRequestBody(changeSetIdString, "ADD""", endpointsArray);  
  219.   
  220.         //Build Header of the Batch Request  
  221.         var batchRequestBody = BuildChangeSetRequestHeader(boundryString, changeSetIdString, userPropertiesBatchBody);  
  222.   
  223.         //Make the REST API call to the _api/$batch endpoint with the batch data  
  224.         console.log("==========================================================")  
  225.         console.log(userPropertiesBatchBody)  
  226.         console.log("==========================================================")  
  227.         console.log(batchRequestBody)  
  228.         console.log("==========================================================")  
  229.         var requestHeaders = {  
  230.             credentials: "include",  
  231.             'X-RequestDigest': FormDigestValue, //r.d.GetContextWebInformation.FormDigestValue,  
  232.             'Content-Type': `multipart/mixed; boundary="batch_${boundryString}"`  
  233.         };  
  234.         return fetch(`${rootUrl}/_api/$batch`, {  
  235.             method: "POST",  
  236.             headers: requestHeaders,  
  237.             body: batchRequestBody  
  238.         }).then(r => r.text()).then(r => {  
  239.             //Convert the text response to an array containing JSON objects of the results  
  240.             var results = parseResponse(r);  
  241.   
  242.             //Properties will be returned in the same sequence they were added to the batch request  
  243.             // for (var i = 0; i < endpointsArray.length; i++) {  
  244.             //     console.log(endpointsArray[i] + " is ", results[i]);  
  245.             // }  
  246.             return results;  
  247.         })  
  248.     }  
  249.     var internalBatch = ({ FormDigestValue, rootUrl, batchUrls }) => {  
  250.         //Reference from  Vardhman Despande blogs https://www.vrdmn.com/2016/06/sharepoint-online-get-userprofile.html  
  251.   
  252.         //AccountName of the user  
  253.         //  var userAccountName = encodeURIComponent("i:0#.f|membership|[email protected]");  
  254.   
  255.         //Collection of Endpoint url to fetch  
  256.         var endpointsArray = batchUrls;//["https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(212)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(213)", "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(214)"]  
  257.   
  258.         //Unique identifier, will be used to delimit different parts of the request body  
  259.         var boundryString = getBoundryString();  
  260.   
  261.         //Build Body of the Batch Request  
  262.         var userPropertiesBatchBody = buildBatchRequestBody(endpointsArray, boundryString);  
  263.   
  264.         //Build Header of the Batch Request  
  265.         var batchRequestBody = buildBatchRequestHeader(userPropertiesBatchBody, boundryString);  
  266.   
  267.         //Make the REST API call to the _api/$batch endpoint with the batch data  
  268.   
  269.         var requestHeaders = {  
  270.             credentials: "include",  
  271.             'X-RequestDigest': FormDigestValue, //r.d.GetContextWebInformation.FormDigestValue,  
  272.             'Content-Type': `multipart/mixed; boundary="batch_${boundryString}"`  
  273.         };  
  274.         return fetch(`${rootUrl}/_api/$batch`, {  
  275.             method: "POST",  
  276.             headers: requestHeaders,  
  277.             body: batchRequestBody  
  278.         }).then(r => r.text()).then(r => {  
  279.             //Convert the text response to an array containing JSON objects of the results  
  280.             var results = parseResponse(r);  
  281.   
  282.             //Properties will be returned in the same sequence they were added to the batch request  
  283.             // for (var i = 0; i < endpointsArray.length; i++) {  
  284.             //     console.log(endpointsArray[i] + " is ", results[i]);  
  285.             // }  
  286.             return results;  
  287.         })  
  288.     }  
  289.     return { GetBatchAll: makeBatchRequests, PostBatchAll: makePostBatchRequests };  
  290. })();  
You can reference this file in your project. This will also work with Spfx and typescript codebase as well. 
 

How to use this Batch Utility for making Batch API Requests in SharePoint Online

 
Step 1
 
Prepare an array of Request URLs. 
  1.  var arr=[  
  2. "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(212)" ,   
  3. "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(213)" ,  
  4.  "https://brgrp.sharepoint.com/_api/Lists/Getbytitle('PlaceHolderList')/items(214)"]  
Step 2
 
Pass the rootUrl or SiteUrl to generate RequestDigest.
  1. var rootUrl= "https://brgrp.sharepoint.com"  
Step 3
 
Pass the information as below and that's it. It will return the result or Request.
  1. BatchUtils.GetBatchAll({rootUrl:rootUrl,batchUrls:arr}) .then(r=>console.log(r))  
You can see an example of response as per the below request.
 
Make SharePoint Online Batch API Request Easy In Single Line Of Code
 
You can find BatchUtils.ts on GitHub as well. 
 

Conclusion 

 
In this article, we have learned about the usage of SharePoint $batch REST API easily using BatchUtils.