Uploading image to Sanity from Next.js API with server-side node.js
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_.
Option 2: API route upload (more secure, recommended)
This is the better approach. You'll need to install multer for handling multipart form data:
npm install multerCreate /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
- The browser
Fileobject doesn't work in API routes - you need to convert it to a Buffer or stream - Use
multipart/form-dataencoding for file uploads - For API routes, use
data.files[0].buffer- this is what Sanity'sassets.upload()accepts in Node.js - Disable Next.js body parser with
bodyParser: falsewhen handling file uploads - 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.
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.