CRUD Operations for SharePoint Docs Using Fetch and REST

In my last post I talked about the REST service calls of what I said at the time was possibly the ugliest SPA of all time. I wanted to do it with no dependencies, which means interacting with XMLHttpRequest directly, and that isn’t anybody’s idea of pretty. No dependencies also means no promises, and once you’ve programmed with promises for a while, working without them on networking code feels like a step backwards. In this post I’m going to rewrite the ugly SPA with the following changes:

  • I’m going to use fetch for all REST calls. fetch is the future, or so they tell me. And it gives me a comfortable promises-based API.
  • I’m also going to use “odata=nometadata” for all of my REST calls. As I mentioned in my last post, this may not work in a SharePoint 2013 environment unless Service Pack 1 has been installed and changes have been made to the SharePoint web.config to support JSON light. So if you’re on 2013 and it doesn’t support JSON light, you need to use “odata=verbose” as shown in my previous post.

As I work through the code, I’ll point out differences between “odata=nometadata” and “odata=verbose”. There really aren’t that many differences.


What We’re Going to Build

Below is the basic interface. It consists of an un-ordered list of picture names, each with a radio button next to it. And at the bottom is a div that will act as a drop zone for files dragged onto it. This interface demonstrates create and read. The un-ordered list is populated by reading the list items in our Picture library, and you can create by dragging images to the drop zone.
 
I already have some images in the library. If you don’t, the un-ordered list will be empty and the only visible thing in the form will be the drop zone to add new files. I’m not actually restricting files to be images, you can drop any file on the zone and it will get uploaded. In fact, you can point to any document library instead of a picture list, except that some of the code expects there to be a Description field, which exists on picture lists but not normally on document libraries.


Ugly SPA

If you click on one of the radio buttons to select an image, a form opens up below the list which provides edit and delete functionality.


Ugly SPA Edit Form

As I said, it’s not pretty, but this gives me enough interface to show the complete range of CRUD operations for SharePoint document libraries.

The HTML

There is nothing special about the HTML, but since I’m going to be manipulating it as I demonstrate the CRUD operations, I’ll show it here so you can refer back to it. It consists of an un-ordered list to show pictures currently in the library, a small form for edit/delete, and a div with some structure to implement drag and drop files.

As I process the read operation and shove list items into the un-ordered list, I’ll also add some hidden data- attributes to the item to store things like the title, description, id, and etag, that will be needed for other CRUD operations later on, so a list item will end up looking like this:

Browser Support

Before we dive into the code, we have to look at how well supported fetch is in browsers. It’s still pretty new as far as browser standards go and browsers have not historically been that quick to implement new standards. You can dig into the gory details on Can I Use Fetch, but to summarize, no version of Internet Explorer supports fetch. Also, fetch is a promises-based API, and no version of Internet Explorer supports the promises API (Can I Use Promises).

Now it’s becoming fashionable to say we’re just not going to support Internet Explorer because of these kinds of shortcomings, but it is not reasonable in a SharePoint application to not support the browsers that Microsoft supports. And often, at this point in the discovery process of browser support for feature X, I conclude that feature X isn’t ready for prime-time and we shouldn’t use it yet. But fetch is the future, it is now supported to some degree by all evergreen browsers, and there are good polyfills available to use it in unsupported browsers, without adding a ton of overhead. And anyway, what the heck, it’s my blog post and I want to play with fetch.

So to get fetch support in most browsers, I include polyfills for Promise and fetch loaded from a CDN like so:

It doesn’t take a rocket scientist to notice that I’ve brought in two different polyfills here in two very different ways, so what gives? Let me ‘splain:

  1. I bring in fetch as a straight-up polyfill so it will define window.fetch if it’s not already defined, and replace it if it is already defined. I do this because it didn’t take long while converting this SPA to fetch, for me to run into problems with the native implementation of fetch on some browsers. So even if the browser implements fetch, it doesn’t mean it implements it well by the standard, or completely. Anyway, the point is, I couldn’t get through basic CRUD without using the polyfill, and I’ll explain where the native implementation of fetch tripped me up when I get to it (hint, it’s in Create).
  2. On the other hand, I chose to use the browser’s native Promise implementation if it has one. Promises are pretty straightforward, and fetch doesn’t do anything crazy with them, so I haven’t seen any issues with the browser implementations of promises that required me to replace the native implementation with a polyfill to do basic CRUD operations.

Fetch 101

There are plenty of good “Intro to fetch” blog posts out there, and I’ll include at least one in the reference section below, so I’m not going to get very deep in my discussion of the fetch API. But I do need to talk about it a little so I can explain why I made specific choices later on, so here goes.

Below is a call to the lists service to get the schema of a list. I made this call in my last post to get the ListEntityTypeFullName, so I could construct the metadata for the changes structure I pass back to SharePoint during the update operation. I won’t actually need that in this post, because I’m switching from “odata=verbose” to “odata=nometadata”, so I don’t need to include metadata in my changes object for updates. Just the same, it’s a pretty simple call for demo purposes, so I’ll use it here:

When I make this call, assuming listTitle is a valid title for a list in my SharePoint site, I get a popup dialog like the one shown below. Obviously, there is a great deal of information coming back. In my last post I added ?$select=ListEntityTypeFullName to the URL, which made the response much smaller. By not specifing any select, I get back the “whole” schema, like so:


List Schema JSON

Now going back to the code, I’d like to point out some things that I don’t necessarily love about the fetch API:

  • Fetch returns a promise, which resolves to a response. The response contains information about both the HTTP request and the HTTP response. To actually get the payload of the response, I have to call either response.json() or response.text(), both of which return another promise.
  • This means that in order to get to what I’m asking for, I have to daisy chain at least two then handlers. If I have a lot of REST calls in my code, that can get annoyingly repetitive.
  • Also, I need to explicitly tell fetch that I want it to include the user’s credentials in the request. The default is not to include them, and fetch doesn’t let me change the defaults. When doing SharePoint REST calls, you can probably pretty well count on always needing to include credentials. Again, annoyingly repetitive.
  • And last, but certainly not least, notice that I’ve got code in the first then handler to check if the response status is outside of the range of success according to HTTP status codes. Fetch doesn’t consider it to be an error and send it to the catch handler unless it’s a real network error, like a failed DNS lookup or failure to connect. A “404 Not Found” status is a perfectly fine HTTP response, so fetch sends it to the then handler. The result is that I have to handle what I consider to be errors in two different places, which obviously isn’t ideal. And again, annoyingly repetitive.

Annoyingly repetitive code is synonymous with “you’re going to be doing a lot of cutting and pasting,” which isn’t going to win you any prizes for coding best practices.

All of this is to say that fetch is a pretty low level API. It doesn’t deal with HTTP error codes, because HTTP is way up at the application layer, and fetch is more down at the network layer. I’ve read about these “deficiencies” in a number of blog posts like Why I won’t be using Fetch API in my apps. I don’t disagree with anything said in that post about fetch shortcomings, in fact I pretty much just made most of the same points, but the title and the conclusion are a bit melodramatic for me.

And I’ve heard others say, if you’re going to wrap fetch, you may as well use a third-part API like axios. But ultimately, I decided to wrap fetch, and in about 35 lines of code I ended up with something that addressed all of these issues to my satisfaction. Axios may be pretty light-weight, but it’s more than 35 lines of code.

A Simple Fetch Wrapper

Below is my fetch wrapper. It’s just a function, called fetchx, that takes the same parameters as fetch (sort of), calls fetch, and returns a promise. It does the following:

  1. First, it tells fetch to always include credentials.
  2. Then, if the init.headers is not of type Headers, it converts it to type Headers, so I can now pass in headers as an arbitrary object used as a map, just like $.ajax or axios.
  3. Then it calls fetch.
  4. It implements a then handler, which does a number of things:
    1. First it checks the HTTP response status and throws an exception on return codes that are obvious errors, which passes off handling to the next catch handler.
    2. Then, if there is no payload, it just returns the response, passing processing off to the next then handler.
    3. Next, if the expected response is “application/json”, it returns the promise from calling response.json(), passing processing to the next then handler. If it expected a JSON response but it doesn’t look like the response is JSON, it throws an exception passing off handling to the next catch handler.
    4. Finally, if it has a payload but doesn’t expect JSON, it returns the promise from response.text(), passing off processing to the next then handler.

Basically, all of the things I identified as annoyingly repetitive are taken care of, one time and in one place in the code. Look Ma’, no cut an paste.

Note I could have add a lot more to the wrapper, removing some of the annoying redundancy from the SharePoint REST API. For instance, how about:

How often, do you imagine, am I going to use the HTTP verb DELETE, but not want the ‘X-HTTP-Method’ also set to DELETE? I can think of at least 25 more lines of SharePoint/REST specific defaults I could add to greatly streamline my usage. I’m not actually going to do any of that in this post, because it would hide the details and make this a pretty poor tutorial.

Create with Fetch

So below is my create call using fetch, and I really don’t have a lot to say about it. The fetch stuff was explained above and the SharePoint REST specific stuff I explained in my last post.

On the other hand, if you’re still with me you may recall that I said earlier that Create was where I ran into problems with at least one browser’s native fetch implementation. When I first started this I had intended to use the native implementation if there was one, but on the latest version of Chrome, this code throws an exception.

Stepping through it in the debugger, I ended up in a big switch/case statement based on the content-type header. Basically, what it said is if the content-type is set to “application/json” go ahead an process it. If it’s anything else, throw a “Not supported” exception. There certainly isn’t anything in the fetch specification that says the content-type must be “application/json”. And we’re uploading an image, it’s a binary payload. There might be some way to format this as JSON that SharePoint would be able to work with, but at a bare minimum it would require something like base64 encoding the binary data, which will bloat the payload and isn’t a very good idea.

Anyway, the result from my point of view is that if I’m going to use fetch, I’m going to polyfill it for now. And until the native implementations of at least all evergreen browsers are more correct and complete, I’ll continue to polyfill it.

Read with Fetch

Read is where you might expect to yield the biggest gains by specifying “odata=nometadata”, since otherwise it would tack on metadata to each item and it could be returning a lot of items. Depending upon your needs, you should prepare to be disappointed. If I call Lists/Items with the request parameters set to ‘$select=FileRef,Title,Description,Id,Created,Modified,GUI’, as I did in my last post, each item comes back looking something like this in the debugger:


Fetch without expanding File.

Ask and ye’ shall receive. But wait! There’s something missing here. Where’s the eTag. In my last post I showed how the eTag comes back as part of of the __metadata. But since we’ve specified “odata=nometadata”, another way of putting that is you get what you ask for. There is another way of getting it though. In a document library, an SPListItem has a File property. To get the lists service to return that file property I have to tell it to expand the file property, by adding &expand=File to the request parameters. When I do that, the items look like this in the debugger:


Fetch with File expanded.

So now we have a file, and the file has an ETag property. It’s not exactly the same as what we got before. It’s a guid, followed by a comma, followed by a number. That number is the eTag that was in the __metadata before.

Now the eTag is only required during update operations, and only if you want to prevent a user from updating something that has already been updated by someone else. If you’re perfectly happy with “last one in wins,” then you can put “*” in the IF-MATCH header (where the eTag goes) on update operations and they will work regardless of version. That’s what I’ve done in my update operation below, but I left the expand file code and the eTag parsing in my Read call just so you can step through it and see an example of expand.

I should point out one other thing. We switched to “odata=nometadata” in an effort to reduce the payload. But expanding the file, as I’ve done, increases the payload by a lot more than specifying “odata=verbose”. There is a way around this. In addition to adding &expand=File, if I add ,File/ETag to the &select, I’ll get back the File with only one property, the ETag. Still, this gets back the eTag for a document. I don’t know how you get it back for a list item without specifying “odata=verbose”. And getting the File.ETag and parsing it is a bit ugly anyway. If I decide I need eTags, I’m probably just going to specify “odata=verbose” for the read operation.

In the code below, I’ve highlighted everything that changed since my last post. That’s mostly the code that parses out the eTag, but there are two other lines that are highlighted. That’s because by switching from “odata=verbose” to “odata=nometadata”, the results come back as an array in json.value instead of in responseJSON.d.results. Other than that, the code is remarkably the same.

Update with Fetch

There isn’t much to say about the update operation. Other than the syntactic differences between the ajax wrapper in my last post and the fetchx wrapper in this one, the only difference is that I specify “*” as the eTag as previously mentioned, so the update will succeed regardless of version.

Delete with Fetch

And finally, below is delete. Other than the syntactic differences between the ajax wrapper and the fetchx wrapper, nothing has changed here.

Sum Up

The end result is that I think I like fetch. On projects where jQuery is already heavily in use, I’m perfectly happy to continue using $.ajax, but if I just want a better Ajax experience than XMLHttpRequest (and who wouldn’t), I’ll probably polyfill fetch for now. And as I continue to blog about SharePoint and REST, it will probably be using fetch.

UglySPAFetch.zip

References

Leave a Comment