There aren’t many examples of how you can upload files (aka ContentVersion entities) to Salesforce using the jsForce library. So I wanted to document what I found while working with the library.
Simplest way
The easiest way to upload a file is to base64 encode the contents and pass it to the create method. Unfortunately this method is limited to around 35MB.
const uploadContentVersion = (fileName: string, file: Buffer): Promise<jsforce.RecordResult> =>
  connection.sobject('ContentVersion').create({
    PathOnClient : fileName,
    VersionData : file.toString('base64')
  })Multipart form data
The trick here is that you need to encode the request correctly as per the example in the documentation.
Example request body
--boundary_string
Content-Disposition: form-data; name="entity_content";
Content-Type: application/json
 
{  
    "Name" : "Marketing Brochure Q1 - Sales",
    "Keywords" : "sales, marketing, first quarter"
}
 
--boundary_string
Content-Type: application/pdf
Content-Disposition: form-data; name="Body"; filename="2011Q1MktgBrochure.pdf"
 
Updated document binary data goes here.
 
--boundary_string--Using the nodejs request library
The popular nodejs `request` library provides a formData option which can be used like below:
import request from 'request'
import jsForce from 'jsforce'
const uploadContentVersion = (metadata: ContentVersionMetadata, file: Buffer): Promise<jsForce.RecordResult> =>
  new Promise((resolve, reject) => {
    const connection: jsForce.Connection = // ...create jsForce connection
    request.post({
      url: connection.instanceUrl + '/services/data/v49.0/sobjects/ContentVersion',
      auth: {
        bearer: connection.accessToken
      },
      formData: {
        entity_content: {
          value: JSON.stringify(metadata),
          options: {
            contentType: 'application/json'
          }
        },
        VersionData: {
          value: file,
          options: {
            filename: metadata.PathOnClient,
            contentType: 'application/octet-stream'
          }
        }
      }
    }, (err, response) => {
      if (err)
        reject(err)
      resolve(JSON.parse(response.body))
    })
  })
Example using `form-data` library
You could also create the formData object manually like this.
  const formData = new FormData()
  formData.append('entity_content', JSON.stringify(metadata), { contentType: 'application/json' })
  formData.append('VersionData', file, {
    filename: metadata.PathOnClient,
    contentType: 'application/octet-stream'
  })
  // then make request using formData object
Full example attaching to Case
export const addFileToCase = async (caseId: string, fileName: string, file: Buffer): Promise<string> => {
  // We use the helper method created previously
  const uploadResult = await uploadContentVersion(file, {
    PathOnClient: fileName,
    // For updates you can also add the following params
    // ContentDocumentId: '',
    // ReasonForChange: ''
  })
  if (!uploadResult.success) {
    throw new Error(`Failed to upload file for case "${caseId}"`)
  }
  const contentDocument = await connection.sobject<{
    Id: string,
    ContentDocumentId: string
  }>('ContentVersion').retrieve(uploadResult.id)
  const linkResult = await connection.sobject<TRes>('ContentDocumentLink').create({
    ContentDocumentId: contentDocument.ContentDocumentId,
    LinkedEntityId: caseId,
    ShareType: 'V'
  })
  if (!linkResult.success) {
    throw new Error(`Failed to link content document for case "${caseId}"`)
  }
  return uploadResult.id
})
Hi, this is an awesome post!
It is right what I was searching to perform smoothly a file migration I’m doing.
I wonder whether you used LWC or nodejs or what else coding to make the coding above.
Could you shed some light on my doubt?
I will thanks a lot!
This was from a NodeJS web service, written in Typescript.
Thank you! can you please see if you can help here: https://stackoverflow.com/questions/72977524/no-preview-for-the-image-i-uploaded-from-nodejs-code-to-salesforce-system
Hi there! Thank you for the post!! It was really helpful!!
The only problem I’m facing is that the .m4a audio file I upload gets corrupted in the process.
Do you know what the problem might be? Thanks!!
Thanks for the help. Fwiw this:
file.toString(‘base64′)
Tripped me up.
When I read a file (fs.readfile) I don’t specify encoding, it seems it handles the binary encoding without issue. When trying to convert toString my uploads end up corrupted. ( regardless of the specified encoding)
Europe, and in Ancient Russia