Skip to content

Building a Fashion App Using Cloudinary’s GenAI in React and Node.js

The FashionistaAI app uses Cloudinary’s GenAI technologies to create a personalized outfit. All you need to do is upload a picture and FashionistaAI will generate four elegant, streetwear, sporty, and business casual outfits styles to suit your unique preferences. 

In this blog post we’ll walk you through how we built the app using Cloudinary’s GenAI features, React SDK on the frontend, and Node.js SDK on the backend to handle image uploads. If you’re interested in fashion, cutting-edge AI, or both, this guide is for you!

An example of a FashionistaAIn output. The background of the images changes to match the model’s outfit.

GitHub Repo: Cloudinary-FashionistaAI

To begin, log in to your Cloudinary account or create a free account. If prompted with the question, “What’s your main interest?”, select Coding with APIs and SDKs or Skip.

In your account, select Settings > Product Environments. Here, you’ll see the cloud that you’ve created. Let’s click the three-dot menu to edit your Cloudinary Cloud Name.

Edit the product environment and name your cloud, then click Save Changes.

It’s important to keep the same cloud name across different tools to be consistent.

In your account, select Settings > Product Environments Setting > API Keys. Click Generate New API Key and save these credentials in a safe place. We’ll use them later.

In this tutorial, I’m using Vite to build my React application. I recommend that you do the same. Follow the instructions on Vite’s official website to create a React application.

In your App.tsx file, replace the existing code with the following:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
import { AdvancedImage } from '@cloudinary/react';
import { fill } from '@cloudinary/url-gen/actions/resize';
import { Cloudinary, CloudinaryImage } from '@cloudinary/url-gen';
import {
  generativeReplace,
  generativeRecolor,
  generativeRestore,
  generativeBackgroundReplace,
} from '@cloudinary/url-gen/actions/effect';

const App: React.FC = () => {
  type StyleKeys = 'top' | 'bottom' | 'background' | 'type';
  const [image, setImage] = useState<any | null>(null);
  const [images, setImages] = useState<CloudinaryImage[]>([]);
  const [error, setError] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);
  const [loadingStatus, setLoadingStatus] = useState<boolean[]>([]); // Track loading status for each image
  const [shouldSubmit, setShouldSubmit] = useState<boolean>(false);
  const [openModal, setOpenModal] = useState(false);
  const [color, setColor] = useState('');
  const [selectedItem, setSelectedItem] = useState<StyleKeys>('top');
  const [selectedImage, setSelectedImage] = useState(0);
  const styles = [
    { top: 'suit jacket for upper body', bottom: 'suit pants for lower body', background: 'office', type: 'business casual' },
    { top: 'sport tshirt for upper body', bottom: 'sport shorts for lower body', background: 'gym', type: 'sporty' },
    { top: 'streetwear shirt for upper body', bottom: 'streetwear pants for lower body', background: 'street', type: 'streetwear' },
    { top: 'elegant tuxedo for upper body', bottom: 'elegant tuxedo pants for lower body', background: 'gala', type: 'elegant' },
  ];

  const cld = new Cloudinary({
    cloud: {
      cloudName: 'YOUR CLOUDINARY CLOUD NAME',
    },
  });

  useEffect(() => {
    if (shouldSubmit && image) {
      handleSubmit();
    }
  }, [shouldSubmit, image]);

  const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files[0]) {
      setImage(e.target.files[0]);
      setShouldSubmit(true);
    }
  };

  const handleSubmit = async () => {
    setImage(null);
    setImages([]);
    setLoadingStatus([]);
    if (!image) {
      alert('Please select an image to upload');
      setShouldSubmit(false);
      return;
    }

    const formData = new FormData();
    formData.append('image', image);

    try {
      setLoading(true);
      const response = await axios.post('/api/generate', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
      const image = cld.image(response.data.public_id);
      image.resize(fill().width(508).height(508));
      setImage(image);
      generateImages(response.data.public_id);
      setLoading(false);
      setError(''); // Clear any previous error messages
    } catch (error) {
      console.error('Error uploading image:', error);
      setError('Error uploading image: ' + (error as Error).message);
      setLoading(false);
    } finally {
      setShouldSubmit(false);
      setLoading(false);
    }
  };

  const handleImageLoading = (image: CloudinaryImage, index: number, attempts = 0) => {
    const url = image.toURL();
    const img = new Image();
    img.src = url;

    img.onload = () => {
      setLoadingStatus((prev) => {
        const newStatus = [...prev];
        newStatus[index] = false; // Image has finished loading
        return newStatus;
      });
    };

    img.onerror = async () => {
      console.error(`Error loading image at index ${index}, Attempt ${attempts + 1}`);

      // Check if 423 status was returned (this requires you to use a proxy server or inspect headers)
      const response = await fetch(url);
      if (response.status === 423) {
        console.log(`423 error received. Retrying image load in 5 seconds... (Attempt ${attempts + 1})`);
        setTimeout(() => handleImageLoading(image, index, attempts + 1), 5000);
      } else {
        console.error('Max retries reached or non-423 error. Image failed to load.');
        setError('Error loading image. Max retries reached.');
        setLoadingStatus((prev) => {
          const newStatus = [...prev];
          newStatus[index] = false; // Stop spinner even if loading fails
          return newStatus;
        });
      }
    };
  };

  const generateImages = (publicId: string) => {
    const genAIImages: CloudinaryImage[] = [];
    const newLoadingStatus: boolean[] = [];

    styles.forEach((style, index) => {
      const image = cld.image(publicId);
      image.effect(generativeReplace().from('shirt').to(style.top));
      image.effect(generativeReplace().from('pants').to(style.bottom));
      image.effect(generativeBackgroundReplace());
      image.effect(generativeRestore());
      image.resize(fill().width(500).height(500));
      genAIImages.push(image);
      newLoadingStatus.push(true); // Set initial loading status
      handleImageLoading(image, index); // Start loading image
    });

    setImages(genAIImages);
    setLoadingStatus(newLoadingStatus);
  };

  const onHandleSelectImage = (index: number) => {
    setOpenModal(!openModal);
    setSelectedImage(index);
  };

  const onHandleChangeItemsColor = () => {
    const genAIImagesCopy = [...images];
    const tempImage = genAIImagesCopy[selectedImage];

    // Show spinner and hide modal
    setLoadingStatus((prev) => {
      const newStatus = [...prev];
      newStatus[selectedImage] = true; // Set loading to true for the selected image
      return newStatus;
    });
    setOpenModal(false); // Close the modal

    // Change color
    tempImage.effect(generativeRecolor(styles[selectedImage][selectedItem], color));

    // Once done, update state and hide spinner
    setImages(genAIImagesCopy);
    handleImageLoading(tempImage, selectedImage); // Trigger reloading of the image
  };

  useEffect(() => {
    console.log('Updated images array:', images); // Log after images state is updated
  }, [images]);

  return (
    <div className="app">
      <h1>Fashionista AI</h1>
      <form onSubmit={(e) => e.preventDefault()}>
        <label className="custom-file-upload">
          <input type="file" accept="image/*" onChange={handleImageChange} />
          Choose File
        </label>
      </form>
      {loading && <div className="spinner"></div>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <div className="container">
        {image && !loading && <AdvancedImage cldImg={image} />}
        <div className="grid-container">
          {images.map((image: CloudinaryImage, index: number) => (
            <div key={index}>
              {loadingStatus[index] ? (
                <div className="spinner"></div>
              ) : (
                <AdvancedImage cldImg={image} onClick={() => onHandleSelectImage(index)} />
              )}
            </div>
          ))}
        </div>
      </div>
      {openModal && (
        <div className="modal-overlay">
          <div className="modal">
            <span className="close-icon" onClick={() => setOpenModal(false)}>
              &times;
            </span>
            <h2>Pick an Item to Change The Color</h2>
            <label>
        <input
          type="radio"
          value="top"
          checked={selectedItem === 'top'}
          onChange={(e)=>setSelectedItem(e.target.value as StyleKeys)}
        />
        Top
      </label>
      <label>
        <input
          type="radio"
          value="bottom"
          checked={selectedItem === 'bottom'}
          onChange={(e)=>setSelectedItem(e.target.value as StyleKeys)}
        />
        Bottom
      </label>
            <input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
            {color && <button onClick={onHandleChangeItemsColor}>Change Item Color</button>}
          </div>
        </div>
      )}
    </div>
  );
};

export default App;Code language: JavaScript (javascript)

Let’s explain our frontend code:

const cld = new Cloudinary({
    cloud: {
      cloudName: 'YOUR CLOUDINARY CLOUD NAME',
    },
  });Code language: JavaScript (javascript)

handleImageChange

const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files && e.target.files[0]) {
    setImage(e.target.files[0]);
    setShouldSubmit(true);
  }
};
Code language: JavaScript (javascript)
  • This function is triggered when a user selects an image from the file input.
  • It checks if a file is selected and, if so, updates the image state with the selected file.
  • The setShouldSubmit(true) triggers the `handleSubmit` function by setting a flag.
const handleSubmit = async () => {
  setImage(null);           
  setImages([]);            
  setLoadingStatus([]);      
  if (!image) {
    alert('Please select an image to upload');
    setShouldSubmit(false);  
    return;
  }

  const formData = new FormData();  
  formData.append('image', image);

  try {
    setLoading(true);
    const response = await axios.post('/api/generate', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });

const image = cld.image(response.data.public_id);      image.resize(fill().width(508).height(508));
setImage(image);                                       generateImages(response.data.public_id);               setLoading(false);                                     setError('');                                        
} catch (error) {
    console.error('Error uploading image:', error);
    setError('Error uploading image: ' + (error as Error).message);
    setLoading(false);  
  } finally {
    setShouldSubmit(false);
    setLoading(false);
  }
};Code language: JavaScript (javascript)
  • This function is responsible for handling the form submission when an image is uploaded.
  • First, it resets the image, images, and loadingStatus state to ensure no residual data from previous uploads.
  • If no image is selected, it alerts the user and stops the submission.
  • If an image is selected, it creates a FormData object and sends the image via a POST request to the /api/generate endpoint.
  • Upon successful upload, it gets the public ID of the uploaded image from the server, creates a CloudinaryImage object, resizes it, and calls generateImages to generate styled images.
  • It handles errors and sets error messages appropriately if the upload fails.
const handleImageLoading = (image: CloudinaryImage, index: number, attempts = 0) => {
  const url = image.toURL();
  const img = new Image();
  img.src = url;

  img.onload = () => {
    setLoadingStatus((prev) => {
      const newStatus = [...prev];
      newStatus[index] = false;
      return newStatus;
    });
  };

  img.onerror = async () => {
    console.error(`Error loading image at index ${index}, Attempt ${attempts + 1}`);

    const response = await fetch(url);
    if (response.status === 423) {
      console.log(`423 error received. Retrying image load in 5 seconds... (Attempt ${attempts + 1})`);
      setTimeout(() => handleImageLoading(image, index, attempts + 1), 5000);
    } else {
      console.error('Max retries reached or non-423 error. Image failed to load.');
      setError('Error loading image. Max retries reached.');
      setLoadingStatus((prev) => {
        const newStatus = [...prev];
        newStatus[index] = false;
        return newStatus;
      });
    }
  };
};
Code language: JavaScript (javascript)
  • This function handles the loading status of each generated image.
  • It checks whether an image successfully loads via img.onload.
  • If the image fails to load due to a network issue or a specific error (such as HTTP status 423), it retries the image loading after five seconds.
    • Initial transformation requests may result in a 423 error response while the transformation is being processed. You can prepare derived versions in advance using an eager transformation.
  • If the image fails multiple times, it gives up and updates the state to stop loading and displays an error.
const generateImages = (publicId: string) => {
  const genAIImages: CloudinaryImage[] = [];
  const newLoadingStatus: boolean[] = [];

  styles.forEach((style, index) => {
   	const image = cld.image(publicId);	image.effect(generativeReplace().from('shirt').to(style.top));     image.effect(generativeReplace().from('pants').to(style.bottom));  
    image.effect(generativeBackgroundReplace());
    image.effect(generativeRestore());
    image.resize(fill().width(500).height(500));
    genAIImages.push(image);
    newLoadingStatus.push(true);
    handleImageLoading(image, index);
  });

  setImages(genAIImages);          
  setLoadingStatus(newLoadingStatus);
};Code language: JavaScript (javascript)
  • This function generates the AI-modified images based on the selected styles.
  • It loops through the predefined styles array, applying generative AI transformations to each image using Cloudinary’s effects:
  • generativeReplace() modifies the clothing items (top and bottom) with the selected style.
  • generativeBackgroundReplace() changes the background.
  • generativeRestore() enhances the image quality.
  • After applying these transformations, it resizes the image and adds it to the genAIImages array.
  • The handleImageLoading function is called to track the loading status of each generated image.
const onHandleSelectImage = (index: number) => {
  setOpenModal(!openModal);
  setSelectedImage(index);
};Code language: JavaScript (javascript)
  • This function opens or closes the recoloring modal when a user clicks on a generated image.
  • It also sets the selectedImage state, which keeps track of which image is currently selected for recoloring.
const onHandleChangeItemsColor = () => {
  const genAIImagesCopy = [...images];
  const tempImage = genAIImagesCopy[selectedImage];

  setLoadingStatus((prev) => {
    const newStatus = [...prev];
    newStatus[selectedImage] = true;
    return newStatus;
  });
  setOpenModal(false);    tempImage.effect(generativeRecolor(styles[selectedImage][selectedItem], color));
  setImages(genAIImagesCopy);
  handleImageLoading(tempImage, selectedImage);
};Code language: JavaScript (javascript)
  • This function is triggered when a user selects a new color for a clothing item (top or bottom).
  • It creates a copy of the images array, applies the generativeRecolor() effect to the selected item with the chosen color, and updates the image.
  • It also sets the loading status for the selected image to show a spinner while the recoloring is processed.

Let’s now style the app. In your app, replace the code inside the App.css with the code inside this file.

If you want to easily make a call to your Node.js, you can configure your Vite app to proxy the backend endpoints. This step only applies if you created the React app using Vite. 

Open your vite.config.js file and replace the content with the following code.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      "/api": {
        target: "http://localhost:8000",
        changeOrigin: true,
        secure: false,
      },
    },
  },
});Code language: JavaScript (javascript)

What we’re doing here is proxying the backend with any path that is /api to be forwarded to the target http://localhost:8000, which is the address of your backend server.

Time to work on the backend. In the root of your React app, create a file called server.js file.

In the root of the project you have the package.json, we will use this same file for our backend. Replace everything that you currently have in the package.json copy and paste the content of this file into your package.json.
In the root of your project create a .env file, and enter the following:

CLOUDINARY_CLOUD_NAME=YOUR CLOUDINARY CLOUD NAME
CLOUDINARY_API_KEY=YOUR CLOUDINARY API KEY
CLOUDINARY_API_SECRET=CLOUDINARY SECRET

Copy and paste the following code into your server.js file.

/* eslint-disable no-undef */
import 'dotenv/config.js';
import express from 'express';
import cors from 'cors';
import { v2 as cloudinary } from 'cloudinary';
import multer from 'multer';
import streamifier from 'streamifier';
import path from 'path'
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
app.use(express.json());
app.use(cors());

// Configure Cloudinary with secure URLs and credentials
cloudinary.config({
  secure: true,
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

// Configure multer for file upload
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

// Define a POST endpoint to handle image upload
app.post('/api/generate', upload.single('image'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'Image file is required' });
  }
  console.log('Uploading image', req.file);

  const uploadStream = cloudinary.uploader.upload_stream(
    {
        resource_type: "image",
    },
     async (error, result) => {
      if (error) {
        console.error('Cloudinary error:', error);
        return res.status(500).json({ error: error.message });
      }
      
      const resObj = {
        ...result
      }
      console.log('Image uploaded to Cloudinary:', resObj);
      res.json(resObj);
    }
  );

  streamifier.createReadStream(req.file.buffer).pipe(uploadStream);
});

app.use(express.static(path.resolve(__dirname, "public")));

const PORT = 8000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
Code language: JavaScript (javascript)

Let’s explain the code we have on our server.

import 'dotenv/config.js';
import express from 'express';
import cors from 'cors';
import { v2 as cloudinary } from 'cloudinary';
import multer from 'multer';
import streamifier from 'streamifier';
import path from 'path'
import { fileURLToPath } from "url";Code language: JavaScript (javascript)
  • dotenv/config.js. Loads environment variables from a .env file.
  • express. Web framework for creating the server.
  • cors. Middleware to enable Cross-Origin Resource Sharing.
  • cloudinary. For image upload and processing.
  • multer. Middleware for handling `multipart/form-data`, used for file uploads.
  • streamifier. Converts a buffer into a readable stream.
  • path, fileURLToPath, fs/promises. Utilities for handling file paths and file operations.
cloudinary.config({
  secure: true,
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});Code language: CSS (css)
  • Configures Cloudinary with secure URLs and credentials from environment variables.
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });Code language: JavaScript (javascript)

Configures Multer to store uploaded files in memory.

app.post('/api/generate', upload.single('image'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'Image file is required' });
  }
  console.log('Uploading image', req.file);

  const uploadStream = cloudinary.uploader.upload_stream(
    {
        resource_type: "image",
    },
     async (error, result) => {
      if (error) {
        console.error('Cloudinary error:', error);
        return res.status(500).json({ error: error.message });
      }
 
      const resObj = {
        ...result
      }
      console.log('Image uploaded to Cloudinary:', resObj);
      res.json(resObj);
    }
  );
 streamifier.createReadStream(req.file.buffer).pipe(uploadStream);
});
   console.error(error);
    return `error: Internal Server Error`;
  }
};
Code language: JavaScript (javascript)
  • Endpoint to handle image upload.
  • Uploads the image to Cloudinary.
  • Returns the Cloudinary object containing the public id for the image.
app.use(express.static(path.resolve(__dirname, "public")));
const PORT = 8000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});Code language: JavaScript (javascript)
  • Serves static files from the “public” directory.
  • Starts the server on port 8000.

This code sets up a comprehensive server for handling image uploads using Cloudinary services.

The first thing we have to do is to run npm install in the root of your project to install the frontend and backend dependencies.

Open your terminal and run npm run start. Nodemon will run your express server in Node.js.

Open your other terminal, run npm run dev, and navigate to http://localhost:3000/.

FashionistaAI showcases how Cloudinary’s GenAI technologies can be integrated into an application to deliver personalized, dynamic visual experiences. Try Cloudinary’s advanced AI features to transform static images into dynamic marketing assets and elevate your content today.

To stay updated with the latest product features, follow Cloudinary on Twitter, explore other sample apps, and join our Discord server and Community forum.

GitHub Repo:  Cloudinary-React-Image-to-Blog-AI

Back to top

Featured Post