File uploads - Allow users of my app to upload assets (PDF or images) through a file picker.

8 replies
Last updated: Aug 11, 2021
I never knew how difficult file uploads could be. My goal is to allow users of my app to upload assets (PDF or images) through a file picker. I want to upload the file to sanity, return the file URL and name in the response, and save the file name and URL into my database so that they can be downloaded or deleted in the future. Is there a tutorial to follow? I’ve read https://www.sanity.io/docs/http-api-assets over and over again, but I do not see a use case where it would work because of the following line of code:
const filePath = '/Users/mike/images/bicycle.jpg'
File path is not available with a file picker. Any direction or ideas would be great.
AI Update

I totally understand your frustration! The HTTP API docs showing a file path example is confusing when you're working with browser file inputs. Good news: you don't need to use file paths at all when uploading from the browser.

The key is using the Sanity JavaScript client with its client.assets.upload() method, which accepts a browser File object directly. Here's how to do it:

Basic Browser Upload Example

import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset',
  token: 'your-write-token', // Important: needs write permissions
  apiVersion: '2024-01-01',
  useCdn: false
})

// Handle file input change
async function handleFileUpload(event) {
  const file = event.target.files[0]
  
  if (!file) return
  
  try {
    // Upload the file directly - works with File object from input!
    const asset = await client.assets.upload('file', file, {
      filename: file.name
    })
    
    // Response contains everything you need:
    console.log('File URL:', asset.url)
    console.log('File name:', asset.originalFilename)
    console.log('Asset ID:', asset._id)
    
    // Save to your database
    await saveToYourDatabase({
      url: asset.url,
      filename: asset.originalFilename,
      assetId: asset._id
    })
    
  } catch (error) {
    console.error('Upload failed:', error)
  }
}

HTML Setup

<input type="file" accept=".pdf,image/*" onChange={handleFileUpload} />

Important Setup Notes

1. Authentication Token: You need a token with write permissions. Create one in your Sanity project settings under API → Tokens.

2. CORS Configuration: If uploading from the browser, add your domain to the CORS origins in your project settings (Sanity Manage → API → CORS Origins).

3. File Size Limits: Sanity has a default upload limit of around 200MB for most plans, but check your specific plan limits.

For Images vs Other Files

The method is slightly different depending on file type:

// For images
const imageAsset = await client.assets.upload('image', file, {
  filename: file.name
})

// For PDFs and other files
const fileAsset = await client.assets.upload('file', file, {
  filename: file.name
})

Complete React Example

import {useState} from 'react'
import {createClient} from '@sanity/client'

const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  token: process.env.SANITY_WRITE_TOKEN,
  apiVersion: '2024-01-01',
  useCdn: false
})

function FileUploader() {
  const [uploading, setUploading] = useState(false)
  const [uploadedFile, setUploadedFile] = useState(null)

  const handleUpload = async (e) => {
    const file = e.target.files[0]
    if (!file) return

    setUploading(true)
    
    try {
      const asset = await client.assets.upload('file', file, {
        filename: file.name
      })
      
      setUploadedFile({
        url: asset.url,
        filename: asset.originalFilename,
        id: asset._id
      })
      
      // Save to your database here
      
    } catch (error) {
      console.error('Upload failed:', error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input 
        type="file" 
        onChange={handleUpload}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
      {uploadedFile && (
        <div>
          <p>Uploaded: {uploadedFile.filename}</p>
          <a href={uploadedFile.url} target="_blank">View File</a>
        </div>
      )}
    </div>
  )
}

Using from Within Sanity Studio

If you're building a custom component inside Sanity Studio, you can use the useClient hook instead:

import {useClient} from 'sanity'

// Inside a React component
const client = useClient({apiVersion: '2025-02-19'})

const file = event.target.files[0]
client.assets
  .upload('file', file)
  .then((document) => {
    console.log('The file was uploaded!', document)
  })
  .catch((error) => {
    console.error('Upload failed:', error.message)
  })

The confusion in the HTTP API docs comes from showing a Node.js example with fs.createReadStream(). In the browser, you simply pass the File object from the file input directly - no file paths needed!

For downloading files later, you can use the URL with the ?dl parameter: ${fileUrl}?dl to force download instead of opening in the browser.

Hey
user U
, as a workaround/temp solution you can upload your asset to a 3rd party service like Firebase, get back a URL then upload that to Sanity.io
Is that what you ended up doing?
Thanks for linking those examples. The flow makes sense, but it sucks having to learn Firebase just to do this.
I assume that you’re doing this outside of the studio,
@sanity/client
works in the browser so you can do something like this:

const file = document.querySelector('input[type="file"]').files[0]

const res = await client.assets.upload('file', file, {
  filename: file.name
})

const { url, assetId } = res
// write the url somewhere else...
File picker return a File object which is a fancy blob so you should be able to pass it straight to the client
I’ll try this tomorrow. Thank you!
Worked like a charm. Thank you.
user G
How are you handling not sharing your token from the frontend? If I were to use a lambda function (netlify), I am limited to a 10 seconds, which may not allow file uploads on slower connections. It seems this is solved with uploading to an S3 bucket with a signed URL. I don’t think sanity does this.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?