100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js & Express Backend
30 minintermediate

Cloudinary and S3 Image Storage

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.

Analogy🏏Cricket
Think of it like cricket: A cricket board that stores match footage only on one laptop in the selection committee room is one hard drive failure away from losing everything. The ICC archive stores footage across multiple data centres worldwide, with automatic backup, regional delivery via edge nodes, and a transformation service that compresses highlights for mobile viewers. Cloudinary is the ICC archive for your app's images — geographically distributed, automatically backed up, and capable of resizing on the fly. S3 is the raw storage vault; Cloudinary is the vault plus a broadcast-quality editing suite.

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.

bash
npm install cloudinary multer
javascript
// 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 };
Analogy🏏Cricket
Think of it like cricket: Multer is the equipment intake desk that unpacks the delivery. streamifier.createReadStream is the conveyor belt that moves the equipment to the inspection machine. Cloudinary's upload_stream is the machine that checks, catalogues, and stores the item in the international archive. The public_id that comes back is the unique accession number you write in your register (database).

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.

Analogy🏏Cricket
🏏 Think of it like cricket: when a player joins the squad you don't re-describe their whole physique every match — the board assigns a permanent cap number that uniquely identifies them forever. That cap number is the Cloudinary public_id: you store it in your database once, and later you can request any transformation of that player (a headshot, a thumbnail, a cropped card) just by quoting the cap number, without re-registering them. Just as the giant scoreboard shows the live, ready-to-read display everyone consumes, the secure_url is the CDN address you hand to the browser. And just as retiring a player means striking their cap number from the register, deletion works off the stored public_id. Keep the cap number and you keep control forever; lose it and you'd have to re-upload the whole player. The payoff: store the public_id, not just the URL, and every future crop, resize, or delete becomes one cheap reference away — no wasted re-uploads.
javascript
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.

Analogy🏏Cricket
🏏 Think of it like cricket: the AWS SDK v3 is modular the way a modern support staff is specialised — you don't hire one person who does everything; you bring in exactly the coach you need for this session. You appoint the S3Client (your team manager), then issue a single specific instruction, the PutObjectCommand (like telling the twelfth man 'carry these pads to locker 14'), and you write down the locker key plus the public gate address so anyone can collect the kit later. Just as some dressing rooms are open to spectators while others need a pass at the gate, a public S3 bucket serves URLs to everyone, but a private bucket needs a pre-signed URL — a time-stamped day-pass that expires after the match so no one wanders in later. The payoff: one command per object, a stored key you can always find it by, and pre-signed passes that grant exactly-timed access without exposing your whole store.
bash
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
javascript
// 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).

Lesson 26 of 36
0% complete