Build a Image Generator


Introduction
What is the first thing you follow when you are learning something new—be it cooking or learning how to code? Tutorials, right?
You’re not alone—25.6% of internet users consume tutorials. Also, 57% of the content you see on the internet is generated by AI. Safe to say that you can accomplish tasks faster than ever with AI as an assistant.
In this tutorial, you will learn how to build a single-page web application which uses AI to generate anything from creating unique digital art, designing marketing posts to even brainstorming concepts for games or films. Think of it as your own version of DALL·E or Midjourney. The possibilities are endless!
Here’s a demo of what you’re going to build
Let’s get buidling! 👷🏻♀️
Pre-requisite: Make sure you have Nodejs already installed on your device already. If you don’t, download it from here: Download | Node.js (nodejs.org).
If you get stuck somewhere, feel free to refer to this repo: https://github.com/aharna/picasso
Run this command in your terminal:
npx create-next-app@latest
If it indicates that you need to install a certain package, just press “y” or Enter.
You can name your project anything but here we’re calling it: Picasso
In the interface that follows, select yes for TypeScript, Tailwind, src/ directory and App Router.
Now create a file called .env
in the root directory itself and enter your key like so:
HF_APIKEY="yoursecretkey"
One very important step is to go to the .gitignore
file in the root directory and add .env
at the bottom. It should look something like this
...
...
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
Cool, now we’re all set up.
Let’s understand the structure of the project first 👇
Frontend
Navigate to src/app/page.tsx
. Remove all the code from there.
Since we want useEffect and useState in our app, which are client-sided hooks, we’ll make the entire page render on the client side.
"use client";
//import from client side
import { useState } from "react";
import Image from "next/image";
//This is the frontend component that will be rendered in the browser.
// Two main sub components-> Input field and Image display
export default function Home() {
const [prompt, setPrompt] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [imageUrl, setImageUrl] = useState("");
const [error, setError] = useState("");
}
Inside the export block, I have added the constants to handle error and included checkpoints to understand where(if) the flow breaks.
const generateImage = async () => {
setIsLoading(true);
setError("");
if (!prompt.trim()) {
setError("Please enter a prompt");
setIsLoading(false);
return;
}
// This try-catch block is used to handle errors that may occur during the API call.
// Hopefully, there is no error :)
try {
const response = await fetch("/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: prompt.trim() }),
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
setImageUrl(url);
console.log("Image generated successfully");
} else {
const errorData = await response.json().catch(() => ({ error: "Failed to generate image" }));
setError(errorData.error || "Failed to generate image");
console.error("API error:", errorData);
}
} catch (error) {
setError("Network error. Please try again.");
console.error("Error occurred during API call:", error);
} finally {
setIsLoading(false);
}
};
This return block is the main design component of the frontend where we render.<input />
allows to enter your text prompt<button>
to generate the image
//This is the main component that will be rendered in the browser.
return (
<main className="flex min-h-screen bg-gray-900 text-white flex-col items-center justify-between px-4 md:px-24 py-12">
<div className="max-w-2xl w-full flex flex-col items-center">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Picasso</h1>
<p className="text-center mb-8 text-gray-300">
This is a fun project that uses Stable Diffusion to generate images from text prompts. <br></br>Just type, and watch your ideas come to life ✨
</p>
<div className="w-full mb-8">
<div className="flex flex-col sm:flex-row gap-2 w-full">
<input
type="text"
className="flex-grow border p-3 text-gray rounded-md border-gray-600 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
placeholder="Enter your image description..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !isLoading && prompt.trim()) {
generateImage();
}
}}
/>
<button
className="bg-blue-600 px-6 py-3 rounded-md font-medium hover:bg-blue-700
disabled:cursor-not-allowed disabled:bg-blue-900 transition-colors"
onClick={generateImage}
disabled={isLoading || !prompt.trim()}
>
{isLoading ? "Generating..." : "🚀 Generate"}
</button>
</div>
{error && (
<p className="text-red-500 mt-2">{error}</p>
)}
</div>
<div className="w-full max-w-md aspect-square relative bg-gray-800 rounded-lg overflow-hidden border border-gray-700">
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : imageUrl ? (
<img
src={imageUrl}
alt={prompt}
className="w-full h-full object-contain"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center flex-col p-6">
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-gray-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-gray-500 text-center">
Your generated image will appear here
</p>
</div>
)}
</div>
</div>
</main>
);
Now run npm run dev
in your terminal and go to http://localhost:3000/ on your browser. You should see the page with all the frontend components. Kudos🎉 Your page should look something like this.
Now, we work on our backend API route to get our application working! Let’s dive 👇
Backend
Navigate to src/app
to create a new folder named “api” and create a route.ts file.
import { NextRequest, NextResponse } from "next/server";
//Important things to note:
//HuggingFace API is used here to generate images from text prompts.
//Ensure that write access is enabled for the API key.
//The API key is stored in the environment variable HF_APIKEY.
export async function POST(req: NextRequest) {
console.log("checkpoint 1: API called");
try {
const request = await req.json();
const prompt = request.prompt as string;
if (!prompt) {
return NextResponse.json(
{ error: "Prompt is required" },
{ status: 400 }
);
}
// Check if API key is available
const apiKey = process.env.HF_APIKEY;
if (!apiKey) {
console.error("Missing Hugging Face API key");
return NextResponse.json(
{ error: "Server configuration error" },
{ status: 500 }
);
}
console.log(`Processing prompt: "${prompt}"`);
const response = await fetch(
"https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5",
{
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
inputs: prompt
}),
}
);
if (!response.ok) {
const errorText = await response.text();
console.error("Hugging Face API error:", errorText);
if (response.status === 403) {
return NextResponse.json(
{ error: "Authentication failed. Please check your Hugging Face API key." },
{ status: 403 }
);
}
return NextResponse.json(
{ error: "Failed to generate image" },
{ status: response.status }
);
}
console.log("checkpoint 2: Image generated successfully");
const imageBlob = await response.blob();
return new NextResponse(imageBlob, {
headers: {
"Content-Type": imageBlob.type,
},
});
} catch (error) {
console.error("Server error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
To securely use Hugging Face's hosted models, we need to include our Hugging Face access keys in the requests along with the prompt. To prevent exposing our API key on the frontend, we route the requests through our backend as an intermediary.
Once we have added our backend, head over to .env
to add your Hugging Face API token.
🎉 TADA! 🎉
Here’s how the final result should look like
Subscribe to my newsletter
Read articles from Aharna Haque directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Aharna Haque
Aharna Haque
Hi! CSE undergrad. Tech & communities also writes sometimes... Partnership & Alliances Zeeve.io prev: contributor @shardeum