SharePoint REST API File Uploads Using JavaScript and PnPjs

Two ways to upload a file to a SharePoint document library via code — raw REST calls, or PnPjs — plus what changes once the file is big enough that a single request isn’t the right approach anymore.


In this post: Option 1: raw REST API · Option 2: PnPjs · Large files: chunked uploads · Validating file names before you upload · Setting metadata after the upload · A React upload component · When would you actually use this? · Related reading


Option 1: raw REST API
function uploadFileToSharePoint(file) {
    const siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite";
    const libraryName = "Documents"; // Update with your library name
    const fileName = file.name;

    fetch(`${siteUrl}/_api/web/getfolderbyserverrelativeurl('${libraryName}')/files/add(overwrite=true, url='${fileName}')`, {
        method: 'POST',
        headers: {
            'Accept': 'application/json;odata=verbose',
            'X-RequestDigest': document.getElementById('__REQUESTDIGEST').value
        },
        body: file
    })
    .then(response => response.json())
    .then(data => console.log("File uploaded successfully", data))
    .catch(error => console.error("Error uploading file: ", error));
}

No dependencies, works in any JavaScript environment (SPFx, classic pages, standalone apps), and gives full control over the request. The tradeoff is that you’re handling the digest token, error handling, and (as covered below) large-file chunking entirely yourself.


Option 2: PnPjs
import { spfi } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/folders";
import "@pnp/sp/files";

const sp = spfi(/* your configured instance */);

async function uploadFileWithPnPjs(file) {
    const libraryName = "Documents";
    try {
        const response = await sp.web.getFolderByServerRelativePath(libraryName)
            .files.addUsingPath(file.name, file, { Overwrite: true });
        console.log("File uploaded successfully", response);
    } catch (error) {
        console.error("Error uploading file: ", error);
    }
}

Less boilerplate, and digest/auth handling is done for you. The real reason to reach for this over raw REST, though, is what’s covered next — large files.


Large files: chunked uploads

The single-request pattern above works fine for typical documents, but SharePoint’s upload limits and general reliability start working against you on bigger files — generally somewhere past 10MB is where a single request gets risky, more so on a slow or unreliable connection. With the raw REST approach, handling that means implementing chunked upload yourself against SharePoint’s StartUpload/ContinueUpload/FinishUpload endpoints. PnPjs wraps this behind one method:

async function uploadLargeFile(file) {
    const libraryName = "Documents";
    try {
        await sp.web.getFolderByServerRelativePath(libraryName)
            .files.addChunked(
                file.name,
                file,
                data => {
                    console.log(`Progress: block ${data.blockNumber}, stage ${data.stage}`);
                },
                true // overwrite
            );
        console.log("Large file uploaded successfully");
    } catch (error) {
        console.error("Error uploading large file: ", error);
    }
}

The default chunk size is 10MB, adjustable via an extra parameter if you’re seeing timeouts on a slower connection (mobile uploads, in particular). The progress callback (the third argument above) is what makes this genuinely useful for a real UI — you get a block number and stage on every chunk, enough to drive an actual progress bar instead of a spinner that doesn’t move for however long a 200MB upload takes.


PnPjs’s addChunked progress callback gives you a block number and stage on every chunk — enough to drive a real progress bar, not just a spinner.

Validating file names before you upload

Neither example above checks the file name before sending it, and SharePoint rejects a real set of characters — not a vague “special characters,” a specific list. Catching this client-side, before the request round-trip, gives a user an actual error message instead of a generic failed-upload state:

function isValidSharePointFileName(fileName) {
    const invalidChars = /["*:<>?/\\|#%]/;
    const reservedNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
    const nameWithoutExt = fileName.split('.').slice(0, -1).join('.');

    if (invalidChars.test(fileName)) {
        return { valid: false, reason: 'Contains a character SharePoint blocks: " * : < > ? / \\ | # %' };
    }
    if (reservedNames.test(nameWithoutExt)) {
        return { valid: false, reason: 'Uses a reserved system name (CON, PRN, AUX, NUL, COM0-9, LPT0-9)' };
    }
    if (fileName.startsWith('~$') || fileName.startsWith(' ') || fileName.endsWith(' ')) {
        return { valid: false, reason: 'Cannot start with ~$ or have leading/trailing spaces' };
    }
    return { valid: true };
}

Two limits worth checking alongside the character validation, since they fail the same unhelpfully-generic way if hit: the full decoded path (library path plus file name, not just the file name alone) can’t exceed 400 characters, and a single file is capped at 250GB regardless of whether it’s going through addUsingPath or addChunked — chunking changes how a large file gets there, not the ceiling on how large it’s allowed to be.


Setting metadata after the upload

Neither upload example above sets any metadata columns beyond the file itself — a library with required or custom fields (a “Department” or “Status” column, for instance) needs a second call against the uploaded file’s list item, since `addUsingPath`/`addChunked` only handle the binary content:

async function uploadWithMetadata(file, metadata) {
    const libraryName = "Documents";
    const uploadResult = await sp.web.getFolderByServerRelativePath(libraryName)
        .files.addUsingPath(file.name, file, { Overwrite: true });

    const item = await uploadResult.file.getItem();
    await item.update(metadata); // e.g. { Department: "Finance", Status: "Draft" }
}

Worth doing this as a genuine second step, not assuming it happens automatically — a library with a required column rejects the file from appearing in a normal view until that field is filled, the same silent-block behavior covered in this site’s sync-limitations content, except here it’s triggered by a programmatic upload instead of OneDrive sync specifically. Worth also knowing a real, documented timing gotcha: calling `getItem()` immediately after `addChunked` specifically (large files) can return a 404 or time out if SharePoint hasn’t finished indexing the newly-uploaded file yet — a short delay or a retry loop before the metadata update call is worth building in for large-file uploads, even though it’s rarely needed after `addUsingPath` on a small file.


A React upload component

Putting the PnPjs approach behind a basic file input and upload button:

import React, { useState } from 'react';
import { spfi } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/folders";
import "@pnp/sp/files";

const sp = spfi(/* your configured instance */);

const FileUpload = () => {
    const [file, setFile] = useState(null);

    const handleFileChange = (e) => {
        setFile(e.target.files[0]);
    };

    const handleUpload = async () => {
        if (file) {
            try {
                await sp.web.getFolderByServerRelativePath("Documents")
                    .files.addUsingPath(file.name, file, { Overwrite: true });
                alert('File uploaded successfully!');
            } catch (error) {
                alert('Error uploading file. Check console for details.');
                console.error(error);
            }
        }
    };

    return (
        
); }; export default FileUpload;

For anything beyond a quick internal tool, swap addUsingPath for addChunked here too and wire the progress callback into component state — users uploading anything beyond a small file benefit from seeing that something’s actually happening.


When would you actually use this?
  • You’re building a custom form (job applications, expense reports) that needs to land an attached file directly in a SharePoint library.
  • You’re automating a backup or migration process and need to push files into SharePoint programmatically rather than dragging them through the browser.
  • Users are uploading files large enough that a single-request upload times out or fails inconsistently — that’s the signal to switch to addChunked.


That covers both approaches and the large-file case. Questions about your specific setup? Drop them in the comments.



App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

Leave a Comment

Your email address will not be published. Required fields are marked *