Cloudinary and S3 Image Storage
Storing user-uploaded files on your local server filesystem is only appropriate for development. In production, files on a single server are lost when the instance is replaced, not shared across multiple instances, and slow to serve compared to a CDN. The two dominant cloud object storage solutions are Amazon S3 (and S3-compatible services like MinIO, Cloudflare R2) and Cloudinary. Cloudinary adds intelligent image transformation and delivery optimisation on top of basic storage.
Cloudinary Upload Flow
Cloudinary provides a Node.js SDK. The upload workflow is: Multer with memoryStorage receives the file buffer, the route handler uploads the buffer to Cloudinary via their API, Cloudinary returns a secure_url and public_id, and you store those in your database. Cloudinary handles format conversion, responsive transformations, and CDN delivery automatically.
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;
// utils/cloudinaryUpload.js
const cloudinary = require('../config/cloudinary');
const streamifier = require('streamifier');
function uploadToCloudinary(buffer, options = {}) {
return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(options, (error, result) => {
if (error) return reject(error);
resolve(result);
});
streamifier.createReadStream(buffer).pipe(stream);
});
}
module.exports = { uploadToCloudinary };Route Handler — Upload and Transform
After uploading, Cloudinary returns a result object containing secure_url (the CDN URL) and public_id (for future transformations or deletion). Store the public_id in your database so you can generate transformed URLs later without re-uploading.
const multer = require('multer');
const { uploadToCloudinary } = require('../utils/cloudinaryUpload');
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 } });
router.post('/products/:id/image',
authenticate,
authorize('write:products'),
upload.single('image'),
async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file provided' });
const result = await uploadToCloudinary(req.file.buffer, {
folder: 'products',
public_id: 'product_' + req.params.id,
overwrite: true,
transformation: [
{ width: 800, height: 800, crop: 'limit' }, // max dimensions
{ quality: 'auto', fetch_format: 'auto' } // auto WebP/AVIF
]
});
await Product.updateOne(
{ _id: req.params.id },
{ imageUrl: result.secure_url, cloudinaryId: result.public_id }
);
res.json({ imageUrl: result.secure_url });
}
);
// Generate responsive URL variants from stored public_id
function getProductImageUrl(publicId, width = 400) {
const cloudinary = require('../config/cloudinary');
return cloudinary.url(publicId, {
width,
crop: 'fill',
quality: 'auto',
fetch_format: 'auto',
secure: true
});
}AWS S3 Upload Flow
AWS S3 uses the AWS SDK v3, which has a modular architecture. You create an S3Client, use the PutObjectCommand to upload a buffer, and store the returned key and the constructed public URL in your database. For private buckets, generate pre-signed URLs for time-limited access.
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner// config/s3.js
const { S3Client } = require('@aws-sdk/client-s3');
module.exports = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
// utils/s3Upload.js
const { PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const s3 = require('../config/s3');
const crypto = require('crypto');
async function uploadToS3(buffer, mimetype, folder = 'uploads') {
const key = folder + '/' + crypto.randomBytes(16).toString('hex');
const cmd = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: buffer,
ContentType: mimetype
});
await s3.send(cmd);
return {
key,
url: 'https://' + process.env.S3_BUCKET + '.s3.' + process.env.AWS_REGION + '.amazonaws.com/' + key
};
}
async function getPresignedUrl(key, expiresIn = 3600) {
const cmd = new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key });
return getSignedUrl(s3, cmd, { expiresIn });
}
module.exports = { uploadToS3, getPresignedUrl };For public image buckets, set the S3 bucket policy to allow s3:GetObject for all principals on the desired key prefix. For private documents, keep the bucket private and generate pre-signed URLs that expire after a short window (15–60 minutes).