Uploading image to Sanity from Next.js API with server-side node.js

10 replies
Last updated: Apr 8, 2020
Trying to upload an image to Sanity from Next.js /api. What am I doing wrong? πŸ˜“
TypeScript doesn't complain about the type, but the error message says it's wrong type.
AI Update

This is a classic issue when uploading images from Next.js API routes to Sanity! The problem you're hitting is that the browser's File object isn't compatible with what Sanity's assets.upload() expects in a Node.js environment (like Next.js API routes).

The Root Cause

When you try to pass a browser File object to a Next.js API route, it gets serialized to an empty object {} because File objects can't be stringified with JSON.stringify(). Even if TypeScript doesn't complain, at runtime Sanity expects a Readable stream, Buffer, or Blob - not the browser's File object.

The Solution: Use Next.js API Routes

Based on this community discussion, here are two approaches:

Option 1: Client-side upload (simpler but less secure)

// Your Sanity client config
export const client = sanityClient({
  projectId: 'your-project-id',
  dataset: 'production',
  apiVersion: '2024-01-01',
  token: process.env.NEXT_PUBLIC_SANITY_WRITE_TOKEN, // Note the NEXT_PUBLIC_ prefix!
});

// In your component
const handleSubmit = async (e) => {
  e.preventDefault();
  
  // Upload the file directly from browser
  const { _id } = await client.assets.upload('image', fileInput);
  
  // Then create your document with the asset reference
  const doc = {
    _type: 'yourDocType',
    image: {
      _type: 'image',
      asset: {
        _type: 'reference',
        _ref: _id,
      },
    },
  };
  
  await client.create(doc);
};

Important: Your token will be exposed in the browser's network tab with this approach. Make sure you prefix your env variable with NEXT_PUBLIC_.

This is the better approach. You'll need to install multer for handling multipart form data:

npm install multer

Create /pages/api/upload.js:

import { client } from '../../lib/sanity';
import multer from 'multer';

async function parseFormData(req, res) {
  const storage = multer.memoryStorage();
  const multerUpload = multer({ storage });
  const multerFiles = multerUpload.any();
  
  await new Promise((resolve, reject) => {
    multerFiles(req, res, (result) => {
      if (result) return reject(result);
      return resolve(result);
    });
  });
  
  return {
    fields: req.body,
    files: req.files,
  };
}

export const config = {
  api: {
    bodyParser: false, // Important!
  },
};

export default async function handler(req, res) {
  const sanityClient = client.withConfig({
    token: process.env.SANITY_WRITE_TOKEN, // No NEXT_PUBLIC prefix needed
  });

  const data = await parseFormData(req, res);

  // Upload the buffer to Sanity
  const { _id } = await sanityClient.assets.upload(
    'image',
    data.files[0].buffer // This is the key - use the buffer!
  );
  
  const doc = {
    _type: 'yourDocType',
    ...data.fields,
    image: {
      _type: 'image',
      asset: {
        _type: 'reference',
        _ref: _id,
      },
    },
  };

  await sanityClient.create(doc);
  res.status(200).json({ success: true });
}

Your form component:

<form action='/api/upload' method='POST' encType='multipart/form-data'>
  <input name='title' type='text' />
  <input type='file' name='image' />
  <button type='submit'>Upload</button>
</form>

Key Takeaways

  1. The browser File object doesn't work in API routes - you need to convert it to a Buffer or stream
  2. Use multipart/form-data encoding for file uploads
  3. For API routes, use data.files[0].buffer - this is what Sanity's assets.upload() accepts in Node.js
  4. Disable Next.js body parser with bodyParser: false when handling file uploads
  5. Your token is more secure in API routes since it doesn't get exposed to the browser

The error message about "wrong type" happens because Sanity's assets.upload() expects different input types depending on the environment - in the browser it can accept File, but in Node.js (API routes) it needs a Buffer, Stream, or Blob.

is this client side or server side?
Server side
This is next.js, and I'm pretty sure the
/pages/api/
will result in a server side node.js thing.
what fetch polyfill/library are you using?
isomorphic-unfetch
I don't think our node client can handle Blobs, any way to convert it to a buffer?
try
r.buffer()
instead of
r.blob()
Ahh, that works! Typescript didn't know about
r.buffer()
only
r.arrayBuffer()
which didn't work.
//@ts-ignore
to the rescue πŸ˜‰
Hehe πŸ˜… I just switched to
node-fetch
since this always will be server side, and now everything types correctly as well πŸ₯³

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?