AI Image Generator Using OpenAI DALL-E API & MERN Stack

Hello there! How do you do? I hope you are doing extremely well. Great!

Without further due let's jump into the amazing project that we're going to build today. It's built upon the great MERN stack.

Stack Used:

  • Express JS

  • React JS

  • MongoDB

  • Node JS

  • Tailwind CSS

  • OpenAI DALL-E API

  • Cloudinary

While doing this project, I can say that I got a feeling of zeel in driving through the process and it's levitating and I hope you feel the same zeel.

I can provide you with the file hierarchy in which this project is going to be built. We start with two folders namely client (frontend) and server (backend) in a root folder of your chosen name.

root

Client

Server

You can get access to every file listed in the above image in the below-provided links to my GitHub repo.

I would like to write the experience here that I've acquired during building this project.

Let's Get Started

Frontend (Client)

  • Beginning with the client folder we focus on building the UI for our project.

UI

That's our homepage where we can see all the AI-generated images shared by people.

You can generate your desired AI image with a prompt of yours and with your name in the checkboxes provided as shown in the below image.

Upload

loader

gen

You can share the image with the world if you want to. And further, it will be stuck in the community gallery where you can download any image you wish.

src client Assets folder consists of some cool images that you need in making this UI look better.

App. jsx

import React from "react";
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";

import { openai } from "./assets";
import { Home, CreatePost } from "./pages";

const App = () => {
  return (
    <BrowserRouter>
      {/* Header */}
      <header className="w-full flex justify-between items-center bg-[#FFCEFE] sm:px-8 px-4 py-4 border-b border-b-[#FFCEFE]">
        <Link to="/">
          <img src={openai} alt="logo" className="h-10 w-10 object-cover" />
        </Link>

        <Link
          to="/create-post"
          className="font-inter font-medium bg-[#a600ff] text-white px-4 py-2 rounded-md"
        >
          Create Post
        </Link>
      </header>

      {/* Body UI */}
      <main className="sm:px-8 px-4 py-8 w-full bg-[#EFEFEF] min-h-[calc(100vh-4.563rem)]">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/create-post" element={<CreatePost />} />
        </Routes>
      </main>
    </BrowserRouter>
  );
};

export default App;

This is the main component which renders a header with an OpenAI logo and a "Create Post" button, and a main section where the different pages of the app are rendered based on the current URL.

The component has two routes: the default route ("/") which renders the Home component, and the "/create-post" route, which renders the CreatePost component.

The component also contains some styling classes which are used to customize the component layout and design.

The components folder provides Card.jsx, FormField.jsx, Loader.jsx.

Card.jsx

import React from "react";

import { download } from "../assets";
import { downloadImage } from "../utils";

const Card = ({ _id, name, prompt, photo }) => {
  return (
    <div className="rounded-xl group relative shadow-card hover:shadow-cardhover card">
      <img
        src={photo}
        alt={prompt}
        className="w-full h-auto object-cover rounded-xl"
      />
      <div className="group-hover:flex flex-col max-h-[94.5%] hidden absolute bottom-0 left-0 right-0 bg-[rgba(255,255,255,0.1)] m-2 p-4 rounded-md">
        <p className="text-white text-sm overflow-y-auto prompt">{prompt}</p>

        <div className="flex justify-between items-center mt-4 gap-2">
          <div className="flex items-center gap-2">
            <div className="w-7 h-7 rounded-full object-cover bg-green-700 flex justify-center items-center text-white text-xs font-bold">
              {name[0]}
            </div>
            <p className="text-white text-xs">{name}</p>
          </div>
          <button
            type="button"
            onClick={() => downloadImage(_id, photo)}
            classname="outline-none bg-transparent border-none"
          >
            <img
              src={download}
              alt="download"
              className="w-6 h-6 object-contain invert"
            />
          </button>
        </div>
      </div>
    </div>
  );
};

export default Card;

This code is a React component that renders a card with an image, prompt, and download button.

The component takes in props of _id, name, prompt, and photo.

It renders an image with the given photo prop as the source and the prompt prop as the alt text.

It also renders a div containing the name prop in a circle with a background color of green-700 and a text color of white.

Lastly, it renders a download button which calls the downloadImage function when clicked and passes in _id and photo as arguments.

FormField.jsx

import React from "react";

const FormField = ({
  labelName,
  type,
  name,
  placeholder,
  value,
  handleChange,
  isSurpriseMe,
  handleSurpriseMe,
}) => {
  return (
    <div>
      <div className="flex items-center gap-2 mb-2">
        <label
          htmlFor={name}
          className="block text-sm font-medium text-gray-900"
        >
          {labelName}
        </label>

        {isSurpriseMe && (
          <button
            type="button"
            onClick={handleSurpriseMe}
            className="font-semibold text-xs bg-[#ECECF1] py-1 px-2 rounded-[5px] text-black"
          >
            Surprise me
          </button>
        )}
      </div>

      <input
        type={type}
        id={name}
        name={name}
        placeholder={placeholder}
        value={value}
        onChange={handleChange}
        required
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-[#4649ff] focus:border-[#4649ff] outline-none block w-full p-3"
      />
    </div>
  );
};

export default FormField;

This code renders a form field. It takes in several props, such as labelName, type, name, placeholder, value, handleChange, isSurpriseMe and handleSurpriseMe.

It then renders an input field with the given props and also renders a button if isSurpriseMe is true.

When the button is clicked it calls the handleSurpriseMe function. I'll tell you what this function does later.

Finally, it exports the FormField component so it can be used in other components.

Loader. jsx

import React from "react";

const Loader = () => (
  <div role="status">
    <svg
      aria-hidden="true"
      className="inline w-10 h-10 mr-2 text-gray-200 animate-spin fill-[#6469ff]"
      viewBox="0 0 100 101"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
        fill="currentColor"
      />
      <path
        d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
        fill="currentFill"
      />
    </svg>
  </div>
);

export default Loader;

This one got that nice cool loading effect when an image gets generated.

index.js

import Card from "./Card";
import FormField from "./FormField";
import Loader from "./Loader";

export { Card, FormField, Loader };

We used an index.js file to export all these components.

Constant's index.js

export const surpriseMePrompts = [
  "an armchair in the shape of an avocado",
  "a surrealist dream-like oil painting by Salvador Dalí of a cat playing checkers",
  "teddy bears shopping for groceries in Japan, ukiyo-e",
  "an oil painting by Matisse of a humanoid robot playing chess",
  "panda mad scientist mixing sparkling chemicals, digital art",
  "a macro 35mm photograph of two mice in Hawaii, they're each wearing tiny swimsuits and are carrying tiny surf boards, digital art",
  "3D render of a cute tropical fish in an aquarium on a dark blue background, digital art",
  "an astronaut lounging in a tropical resort in space, vaporwave",
  "an oil painting portrait of a capybara wearing medieval royal robes and an ornate crown on a dark background",
  "a stained glass window depicting a hamburger and french fries",
  "a pencil and watercolor drawing of a bright city in the future with flying cars",
  "a sunlit indoor lounge area with a pool with clear water and another pool with translucent pastel pink water, next to a big window, digital art",
  "a fortune-telling shiba inu reading your fate in a giant hamburger, digital art",
  '"a sea otter with a pearl earring" by Johannes Vermeer',
  "an oil pastel drawing of an annoyed cat in a spaceship",
  "a painting of a fox in the style of Starry Night",
  "a bowl of soup that looks like a monster, knitted out of wool",
  "A plush toy robot sitting against a yellow wall",
  "A synthwave style sunset above the reflecting water of the sea, digital art",
  "Two futuristic towers with a skybridge covered in lush foliage, digital art",
  "A 3D render of a rainbow colored hot air balloon flying above a reflective lake",
  "A comic book cover of a superhero wearing headphones",
  "A centered explosion of colorful powder on a black background",
  "A photo of a Samoyed dog with its tongue out hugging a white Siamese cat",
  "A photo of a white fur monster standing in a purple room",
  "A photo of Michelangelo's sculpture of David wearing headphones djing",
  "A Samurai riding a Horse on Mars, lomography.",
  "A modern, sleek Cadillac drives along the Gardiner expressway with downtown Toronto in the background, with a lens flare, 50mm photography",
  "A realistic photograph of a young woman with blue eyes and blonde hair",
  "A man standing in front of a stargate to another dimension",
  "Spongebob Squarepants in the Blair Witch Project",
  "A velociraptor working at a hotdog stand, lomography",
  "A man walking through the bustling streets of Kowloon at night, lit by many bright neon shop signs, 50mm lens",
  "A BBQ that is alive, in the style of a Pixar animated movie",
  "A futuristic cyborg dance club, neon lights",
  "The long-lost Star Wars 1990 Japanese Anime",
  "A hamburger in the shape of a Rubik’s cube, professional food photography",
  "A Synthwave Hedgehog, Blade Runner Cyberpunk",
  "An astronaut encountering an alien life form on a distant planet, photography",
  "A Dinosaur exploring Cape Town, photography",
  "A Man falling in Love with his Computer, digital art",
  "A photograph of a cyborg exploring Tokyo at night, lomography",
  "Dracula walking down the street of New York City in the 1920s, black and white photography",
  "Synthwave aeroplane",
  "A man wanders through the rainy streets of Tokyo, with bright neon signs, 50mm",
  "A Space Shuttle flying above Cape Town, digital art",
];

This is a bunch of prompts in which one is randomly given to the person when they hit the SurpriseMe button.

PAGES

CreatePost.jsx

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

import { preview } from "../assets";
import { getRandomPrompt } from "../utils";
import { FormField, Loader } from "../components";

const CreatePost = () => {
  const navigate = useNavigate();
  const [form, setForm] = useState({
    name: "",
    prompt: "",
    photo: "",
  });
  const [generatingImg, setGeneratingImg] = useState(false);
  const [loading, setLoading] = useState(false);

  // Integrate with the backend
  const generateImage = async () => {
    if (form.prompt) {
      try {
        setGeneratingImg(true);
        const response = await fetch(
          "https://dall-e-4e72.onrender.com/api/v1/dalle",
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ prompt: form.prompt }),
          }
        );

        const data = await response.json();

        setForm({ ...form, photo: `data:image/jpeg;base64,${data.photo}` });
      } catch (error) {
        alert("Something went wrong");
      } finally {
        setGeneratingImg(false);
      }
    } else {
      alert("Please enter a prompt");
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (form.prompt && form.photo) {
      setLoading(true);

      try {
        const response = await fetch(
          "https://dall-e-4e72.onrender.com/api/v1/post",
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(form),
          }
        );

        await response.json();
        navigate("/");
      } catch (error) {
        alert("Something went wrong");
      } finally {
        setLoading(false);
      }
    } else {
      alert("Please enter a prompt and generate an image");
    }
  };

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleSurpriseMe = () => {
    const randomPrompt = getRandomPrompt(form.prompt);
    setForm({ ...form, prompt: randomPrompt });
  };

  return (
    <section className="max-w-7xl mx-auto">
      <div>
        <h1 className="font-extrabold text-[#222328] text-[2rem]">
          Create Post
        </h1>

        <p className="mt-2 text-[#666e75] text-[1rem] max-w-[31.25rem]">
          Create imaginative and visually stunning images through DALL-E AI and
          share them with the world.
        </p>
      </div>

      <form className="mt-16 max-w-3xl" onSubmit={handleSubmit}>
        <div className="flex flex-col gap-5">
          <FormField
            labelName="Your Name"
            type="text"
            name="name"
            placeholder="Eg. Jagadeesh KJ"
            value={form.name}
            handleChange={handleChange}
          />

          <FormField
            labelName="Prompt"
            type="text"
            name="prompt"
            placeholder="Eg. An armchair in the shape of an avocado"
            value={form.prompt}
            handleChange={handleChange}
            isSurpriseMe
            handleSurpriseMe={handleSurpriseMe}
          />

          <div className="relative bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-64 p-3 h-64 flex justify-center items-center">
            {form.photo ? (
              <img
                src={form.photo}
                alt={form.prompt}
                className="w-full h-full object-contain"
              />
            ) : (
              <img
                src={preview}
                alt="preview"
                className="w-9/12 h-9/12 object-contain opacity-40"
              />
            )}

            {generatingImg && (
              <div className="absolute inset-0 z-0 flex justify-center items-center bg-[rgba(0,0,0,0.5)] rounded-lg">
                <Loader />
              </div>
            )}
          </div>
        </div>

        <div className="mt-5 flex gap-5">
          <button
            type="button"
            onClick={generateImage}
            className="text-white bg-green-700 font-medium rounded-md text-sm w-full sm:w-auto px-5 py-2.5 text-center"
          >
            {generatingImg ? "Generating..." : "Generate Image"}
          </button>
        </div>

        <div className="mt-10">
          <p className="mt-2 text-[#666e75] text-[0.875rem]">
            Once you have created the image, you can share it with the world.
          </p>
          <button
            type="submit"
            className="mt-3 text-white bg-[#6469ff] font-medium rounded-md text-sm w-full sm:w-auto px-5 py-2.5 text-center"
          >
            {loading ? "Sharing..." : "Share"}
          </button>
        </div>
      </form>
    </section>
  );
};

export default CreatePost;

This component is really important as it takes input from the person and gives them a chance to create an image by DALL-E.

The CreatePost component is defined, which uses the useNavigate and useState hooks to handle navigation and state management.

The initial state includes an empty form object, with properties for name, prompt, and photo. The component also has state variables for generatingImg and loading.

The generateImage function sends a POST request to the DALL-E API, which returns an image based on the prompt. The handleSubmit function sends a POST request to the backend to create a post.

The handleChange function updates the form state with the input values. The handleSurpriseMe function sets the prompt field to a random prompt, and the render method returns JSX that renders the form and other UI elements.

Home. jsx

import React, { useState, useEffect } from "react";

import { Loader, Card, FormField } from "../components";

const RenderCards = ({ data, title }) => {
  if (data?.length > 0) {
    return data.map((post) => <Card key={post._id} {...post} />);
  }

  return (
    <h2 className="mt-5 font-bold text-[#6449ff] text-xl uppercase">{title}</h2>
  );
};

const Home = () => {
  const [loading, setLoading] = useState(false);
  const [allPosts, setAllPosts] = useState(null);

  const [searchText, setSearchText] = useState("");
  const [searchedResults, setSearchedResults] = useState(null);
  const [searchTimeout, setSearchTimeout] = useState(null);

  // Integrate with the backend to fetch all posts
  useEffect(() => {
    const fetchPosts = async () => {
      setLoading(true);

      try {
        const response = await fetch(
          "https://dall-e-4e72.onrender.com/api/v1/post",
          {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
            },
          }
        );

        if (response.ok) {
          const result = await response.json();
          setAllPosts(result.data.reverse());
        }
      } catch (error) {
        alert("Something went wrong");
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []);

  // Search functionality
  const handleSearchChange = (e) => {
    clearTimeout(searchTimeout);

    setSearchText(e.target.value);

    setSearchTimeout(
      setTimeout(() => {
        const searchResults = allPosts.filter(
          (item) =>
            item.name.toLowerCase().includes(searchText.toLowerCase()) ||
            item.prompt.toLowerCase().includes(searchText.toLowerCase())
        );

        setSearchedResults(searchResults);
      }, 500)
    );
  };

  return (
    <section className="max-w-7xl mx-auto">
      <div>
        <h1 className="font-extrabold text-[#222328] text-[2rem]">
          DALL-E AI Gallery
        </h1>
        <p className="mt-2 text-[#666e75] text-[1rem]">
          Browse through the images generated by DALL-E AI. You can also create
          a new post and share your own images.
        </p>
      </div>

      <div className="mt-16">
        <FormField
          labelName="Search Posts"
          type="text"
          name="text"
          placeholder="Search by name or prompt"
          value={searchText}
          handleChange={handleSearchChange}
        />
      </div>

      <div className="mt-10">
        {loading ? (
          <div className="flex justify-center items-center">
            <Loader />
          </div>
        ) : (
          <>
            {searchText && (
              <h2 className="font-medium text-[#666e75] text-xl mb-3">
                Search Results for{" "}
                <span className="text-[#222328]">{searchText}</span>
              </h2>
            )}

            <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
              {searchText ? (
                <RenderCards data={searchedResults} title="No results found" />
              ) : (
                <RenderCards data={allPosts} title="No posts found" />
              )}
            </div>
          </>
        )}
      </div>
    </section>
  );
};

export default Home;

The "Home" component sets up several state variables, including "loading", "allPosts", "searchText", "searchedResults", and "searchTimeout", using the "useState" hook.

It also uses the "useEffect" hook to fetch all posts from an API when the component first renders and sets the state variables accordingly.

The component also includes search functionality, where the "handleSearchChange" function is called when the user types into the search field.

The component then renders either the searched results or all posts and includes a loading spinner if the data is still being fetched from the API.

PAGES index.js

import Home from "./Home";
import CreatePost from "./CreatePost";

export { Home, CreatePost };

UTILS index.js

import FileSaver from "file-saver";

import { surpriseMePrompts } from "../constant";

export function getRandomPrompt() {
  const randomIndex = Math.floor(Math.random() * surpriseMePrompts.length);
  const randomPrompt = surpriseMePrompts[randomIndex];

  //   To not get the same prompt twice in a row
  if (randomPrompt === prompt) return getRandomPrompt(prompt);

  return randomPrompt;
}

export async function downloadImage(_id, photo) {
  FileSaver.saveAs(photo, `download-${_id}.jpg`);
}

The code importing the FileSaver library which is used to save a file to the user's device. It also imports an array of strings named "surpriseMePrompts" from a separate file.

The function "downloadImage" takes in two parameters, id and photo. It then uses the FileSaver library to save the "photo" file to the user's device with the file name "download-id.jpg".

I've used MongoDB for storing the images after retrieving them from the Cloudinary after generating an image using OpenAI API.

Render is a great hosting cloud platform that we can use for deploying the backend and I've used vercel for the frontend.

A tip I can leave that I've found useful while choosing a color palette for the website is Visme.

That's all to it, folks. I wanted to show my gratitude to JavaScript Mastery for making this wonderful MERN stack web app possible. He's just awesome.

Repo ⚡

Live 🚀

I'm gonna wrap it up with links provided to my GitHub repo and to the live deployment. See you soon!