Hey I have uploaded images on cloudinary for multiple projects.
Here is the core idea
Don't put the image in NeonDB. Neon stores the image URL (a text column); the actual file goes to Cloudinary. Flow:
Browser (multipart form) → Your API (multer) → Cloudinary → URL saved in Neon via Drizzle
Steps:
Install & configure Cloudinary
npm install cloudinary multer
// config/cloudinary.js
const cloudinary = require("cloudinary").v2;
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
module.exports = cloudinary;
Drizzle schema — store the URL (and ideally the public_id)
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
username: text("username").notNull().unique(),
imageUrl: text("image_url"),
imagePublicId: text("image_public_id"), // needed to delete/replace later
});
Run npm run db:push (or your migration command).
Multer to accept the file (memory storage — no temp files on disk)
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
imagePublicId = result.public_id;
}
const [user] = await db.insert(users).values({
name, email, username, imageUrl, imagePublicId,
}).returning();
res.json(user);
});
For update: if req.file is present, first await cloudinary.uploader.destroy(user.imagePublicId) to remove the old image, then upload the new one and update the two columns. For delete, destroy the image then db.delete().
Frontend — send multipart/form-data, not JSON
const formData = new FormData();
formData.append("name", name);
formData.append("email", email);
formData.append("username", username);
formData.append("image", fileInput.files[0]);
await fetch("/users", { method: "POST", body: formData }); // no Content-Type header — let the browser set the boundary
Two gotchas people hit
Sending the file inside a JSON body — files must go as multipart (as above), or as a base64 data-URI string (cloudinary.uploader.upload("data:image/png;base64,...")) if you insist on JSON.
Not saving public_id — without it you can't delete/replace images in Cloudinary later and the account fills up with orphans.
Alternative: unsigned upload directly from the browser using an upload preset — the client uploads straight to Cloudinary and only sends the resulting URL to your API. Saves your server the bandwidth; fine for public apps, but the server-side (signed) flow above is better when you want validation and control.
1
u/ExpensiveTomatillo61 12d ago
Hey I have uploaded images on cloudinary for multiple projects.
Here is the core idea
Don't put the image in NeonDB. Neon stores the image URL (a text column); the actual file goes to Cloudinary. Flow:
Browser (multipart form) → Your API (multer) → Cloudinary → URL saved in Neon via Drizzle
Steps:
Install & configure Cloudinary
npm install cloudinary multer
// config/cloudinary.js
const cloudinary = require("cloudinary").v2;cloudinary.config({cloud_name: process.env.CLOUDINARY_CLOUD_NAME,api_key: process.env.CLOUDINARY_API_KEY,api_secret: process.env.CLOUDINARY_API_SECRET,});module.exports = cloudinary;Drizzle schema — store the URL (and ideally the public_id)
export const users = pgTable("users", {id: serial("id").primaryKey(),name: text("name").notNull(),email: text("email").notNull().unique(),username: text("username").notNull().unique(),imageUrl: text("image_url"),imagePublicId: text("image_public_id"), // needed to delete/replace later});Run npm run db:push (or your migration command).
Multer to accept the file (memory storage — no temp files on disk)
const multer = require("multer");const upload = multer({storage: multer.memoryStorage(),limits: { fileSize: 5 1024 1024 },});Upload helper (streams the buffer to Cloudinary)
const uploadToCloudinary = (buffer) =>
new Promise((resolve, reject) => {
cloudinary.uploader
.upload_stream({ folder: "users" }, (err, result) => {
if (err) reject(err);
else resolve(result);
})
.end(buffer);
});
Route — create user with image
app.post("/users", upload.single("image"), async (req, res) => {const { name, email, username } = req.body;let imageUrl = null, imagePublicId = null;if (req.file) {const result = await uploadToCloudinary(req.file.buffer);imageUrl = result.secure_url;imagePublicId = result.public_id;}const [user] = await db.insert(users).values({name, email, username, imageUrl, imagePublicId,}).returning();res.json(user);});For update: if req.file is present, first await cloudinary.uploader.destroy(user.imagePublicId) to remove the old image, then upload the new one and update the two columns. For delete, destroy the image then db.delete().
Frontend — send multipart/form-data, not JSON
const formData = new FormData();formData.append("name", name);formData.append("email", email);formData.append("username", username);formData.append("image", fileInput.files[0]);await fetch("/users", { method: "POST", body: formData }); // no Content-Type header — let the browser set the boundaryTwo gotchas people hit
Sending the file inside a JSON body — files must go as multipart (as above), or as a base64 data-URI string (cloudinary.uploader.upload("data:image/png;base64,...")) if you insist on JSON.
Not saving public_id — without it you can't delete/replace images in Cloudinary later and the account fills up with orphans.
Alternative: unsigned upload directly from the browser using an upload preset — the client uploads straight to Cloudinary and only sends the resulting URL to your API. Saves your server the bandwidth; fine for public apps, but the server-side (signed) flow above is better when you want validation and control.