El problema de las imágenes en producción
Guardar imágenes en el servidor local funciona en desarrollo. En producción es un desastre: si escala horizontalmente tenés imágenes repartidas en múltiples instancias, si el servidor se cae perdés los archivos, y no tenés forma sencilla de hacer transformaciones (resize, webp, etc.).
Cloudinary resuelve todos estos problemas con una API simple. Lo uso en absolutamente todos mis proyectos.
Setup en Node.js
npm install cloudinary multer multer-storage-cloudinary
# .env
CLOUDINARY_CLOUD_NAME=tu_cloud_name
CLOUDINARY_API_KEY=tu_api_key
CLOUDINARY_API_SECRET=tu_api_secret
// services/cloudinary.js
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
export default cloudinary;
Upload con Multer
import multer from 'multer';
import { CloudinaryStorage } from 'multer-storage-cloudinary';
import cloudinary from './cloudinary.js';
const storage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'mi-app/uploads',
allowed_formats: ['jpg', 'jpeg', 'png', 'webp'],
transformation: [{ width: 1200, crop: 'limit', quality: 'auto' }],
},
});
export const upload = multer({ storage });
// Uso en ruta:
router.post('/foto', upload.single('image'), async (req, res) => {
const { secure_url, public_id } = req.file;
// Guardar en MongoDB
res.json({ url: secure_url, publicId: public_id });
});
Transformaciones on-the-fly en la URL
Una de las features más poderosas: podés transformar imágenes directamente desde la URL sin procesamiento adicional.
// URL original
https://res.cloudinary.com/mi-cloud/image/upload/v123/foto.jpg
// Thumbnail 200x200
https://res.cloudinary.com/mi-cloud/image/upload/w_200,h_200,c_fill/v123/foto.jpg
// Webp automático, calidad auto, ancho máximo 800
https://res.cloudinary.com/mi-cloud/image/upload/f_auto,q_auto,w_800/v123/foto.jpg
Borrar imágenes al actualizar
export const deleteImage = async (publicId) => {
if (!publicId) return;
await cloudinary.uploader.destroy(publicId);
};
// Al actualizar foto de perfil:
if (user.profilePhoto?.publicId) {
await deleteImage(user.profilePhoto.publicId);
}
const { secure_url, public_id } = req.file;
user.profilePhoto = { url: secure_url, publicId: public_id };