In this comprehensive tutorial, we’ll create a modern AI image generator using Next.js, integrating with popular AI image generation APIs. By the end of this guide, you’ll have a fully functional web application that can generate stunning images from text prompts.
What We’re Building
Our AI image generator will feature:
- Clean, modern UI with real-time image generation
- Integration with AI image generation APIs (OpenAI DALL-E, Stability AI, or Replicate)
- Image download functionality
- Responsive design optimized for all devices
- Loading states and error handling
- Image history and gallery view
Prerequisites
Before we start, make sure you have:
- Node.js 18+ installed
- Basic knowledge of React and JavaScript
- An API key from your chosen AI service (we’ll use OpenAI DALL-E in this example)
- A code editor (VS Code recommended)
Project Setup
1. Create a New Next.js Project
npx create-next-app@latest ai-image-generator
cd ai-image-generator
Choose the following options when prompted:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
- Customize default import alias: No
2. Install Required Dependencies
npm install openai axios lucide-react
npm install -D @types/node
3. Environment Setup
Create a .env.local file in your project root:
OPENAI_API_KEY=your_openai_api_key_here
NEXT_PUBLIC_APP_URL=http://localhost:3000
Building the Core Components
1. Create the API Route
First, let’s create an API endpoint to handle image generation requests. Create app/api/generate-image/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(request: NextRequest) {
try {
const { prompt, size = "1024x1024", quality = "standard" } = await request.json();
if (!prompt) {
return NextResponse.json(
{ error: 'Prompt is required' },
{ status: 400 }
);
}
const response = await openai.images.generate({
model: "dall-e-3",
prompt,
n: 1,
size: size as "1024x1024" | "1792x1024" | "1024x1792",
quality: quality as "standard" | "hd",
});
const imageUrl = response.data[0]?.url;
if (!imageUrl) {
throw new Error('No image URL returned from OpenAI');
}
return NextResponse.json({
success: true,
imageUrl,
prompt
});
} catch (error: any) {
console.error('Image generation error:', error);
return NextResponse.json(
{
error: 'Failed to generate image',
details: error.message
},
{ status: 500 }
);
}
}
2. Create the Image Generator Component
Create components/ImageGenerator.tsx:
'use client';
import { useState } from 'react';
import { Download, Wand2, Loader2, Image as ImageIcon } from 'lucide-react';
interface GeneratedImage {
url: string;
prompt: string;
timestamp: number;
}
export default function ImageGenerator() {
const [prompt, setPrompt] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const [generatedImages, setGeneratedImages] = useState<GeneratedImage[]>([]);
const [error, setError] = useState<string | null>(null);
const [selectedSize, setSelectedSize] = useState<'1024x1024' | '1792x1024' | '1024x1792'>('1024x1024');
const [selectedQuality, setSelectedQuality] = useState<'standard' | 'hd'>('standard');
const generateImage = async () => {
if (!prompt.trim()) {
setError('Please enter a prompt');
return;
}
setIsGenerating(true);
setError(null);
try {
const response = await fetch('/api/generate-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt.trim(),
size: selectedSize,
quality: selectedQuality,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to generate image');
}
const newImage: GeneratedImage = {
url: data.imageUrl,
prompt: data.prompt,
timestamp: Date.now(),
};
setGeneratedImages(prev => [newImage, ...prev]);
} catch (err: any) {
console.error('Generation failed:', err);
setError(err.message || 'Failed to generate image');
} finally {
setIsGenerating(false);
}
};
const downloadImage = async (imageUrl: string, prompt: string) => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `ai-generated-${prompt.slice(0, 30).replace(/[^a-z0-9]/gi, '_').toLowerCase()}.png`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Download failed:', error);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
generateImage();
}
};
return (
<div className="max-w-4xl mx-auto p-6 space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-100 to-pink-100 rounded-full">
<Wand2 className="w-5 h-5 text-purple-600" />
<span className="text-purple-800 font-medium">AI Image Generator</span>
</div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
Create Stunning AI Images
</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
Transform your ideas into beautiful images using the power of artificial intelligence.
Just describe what you want to see, and watch as AI brings it to life.
</p>
</div>
{/* Generation Form */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8">
<div className="space-y-6">
{/* Prompt Input */}
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-2">
Describe your image
</label>
<textarea
id="prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="A majestic mountain landscape at sunset with purple clouds..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none transition-all duration-200"
rows={3}
disabled={isGenerating}
/>
</div>
{/* Options */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Image Size
</label>
<select
value={selectedSize}
onChange={(e) => setSelectedSize(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isGenerating}
>
<option value="1024x1024">Square (1024×1024)</option>
<option value="1792x1024">Landscape (1792×1024)</option>
<option value="1024x1792">Portrait (1024×1792)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Quality
</label>
<select
value={selectedQuality}
onChange={(e) => setSelectedQuality(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isGenerating}
>
<option value="standard">Standard</option>
<option value="hd">HD (Higher Cost)</option>
</select>
</div>
</div>
{/* Generate Button */}
<button
onClick={generateImage}
disabled={isGenerating || !prompt.trim()}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold py-3 px-6 rounded-lg hover:from-purple-700 hover:to-pink-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center gap-2"
>
{isGenerating ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Generating...
</>
) : (
<>
<Wand2 className="w-5 h-5" />
Generate Image
</>
)}
</button>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
</div>
</div>
{/* Generated Images Gallery */}
{generatedImages.length > 0 && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
<ImageIcon className="w-6 h-6" />
Your Generated Images
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{generatedImages.map((image, index) => (
<div key={index} className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="aspect-square relative">
<img
src={image.url}
alt={image.prompt}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4 space-y-3">
<p className="text-gray-700 text-sm line-clamp-2">{image.prompt}</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{new Date(image.timestamp).toLocaleString()}
</span>
<button
onClick={() => downloadImage(image.url, image.prompt)}
className="flex items-center gap-1 px-3 py-1 bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors text-sm"
>
<Download className="w-4 h-4" />
Download
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
3. Update the Main Page
Replace the content of app/page.tsx:
import ImageGenerator from '../components/ImageGenerator';
export default function Home() {
return (
<main className="min-h-screen bg-gradient-to-br from-purple-50 via-pink-50 to-indigo-50">
<div className="container mx-auto py-8">
<ImageGenerator />
</div>
</main>
);
}
4. Update the Layout
Update app/layout.tsx to include proper metadata:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'AI Image Generator',
description: 'Create stunning images using artificial intelligence',
keywords: 'AI, image generation, DALL-E, artificial intelligence',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
Advanced Features
1. Add Image History with Local Storage
Create hooks/useImageHistory.ts:
import { useState, useEffect } from 'react';
interface GeneratedImage {
url: string;
prompt: string;
timestamp: number;
}
export function useImageHistory() {
const [images, setImages] = useState<GeneratedImage[]>([]);
useEffect(() => {
const saved = localStorage.getItem('ai-generated-images');
if (saved) {
try {
setImages(JSON.parse(saved));
} catch (error) {
console.error('Failed to parse saved images:', error);
}
}
}, []);
const addImage = (image: GeneratedImage) => {
const newImages = [image, ...images].slice(0, 50); // Keep only last 50 images
setImages(newImages);
localStorage.setItem('ai-generated-images', JSON.stringify(newImages));
};
const clearHistory = () => {
setImages([]);
localStorage.removeItem('ai-generated-images');
};
return {
images,
addImage,
clearHistory,
};
}
2. Add Prompt Suggestions
Create components/PromptSuggestions.tsx:
'use client';
interface PromptSuggestionsProps {
onSelectPrompt: (prompt: string) => void;
}
const PROMPT_SUGGESTIONS = [
"A futuristic cityscape at night with neon lights and flying cars",
"A magical forest with glowing mushrooms and fairy lights",
"A steampunk-inspired mechanical dragon breathing steam",
"An underwater palace with coral walls and swimming mermaids",
"A cozy mountain cabin in winter with snow falling outside",
"A space station orbiting a colorful nebula",
"A vintage train traveling through autumn countryside",
"A cyberpunk street market with holographic signs",
];
export default function PromptSuggestions({ onSelectPrompt }: PromptSuggestionsProps) {
return (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">Popular Prompts</h3>
<div className="flex flex-wrap gap-2">
{PROMPT_SUGGESTIONS.map((prompt, index) => (
<button
key={index}
onClick={() => onSelectPrompt(prompt)}
className="px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 transition-colors"
>
{prompt.length > 40 ? `${prompt.slice(0, 40)}...` : prompt}
</button>
))}
</div>
</div>
);
}
3. Add Loading Animation
Create components/LoadingSpinner.tsx:
export default function LoadingSpinner() {
return (
<div className="flex flex-col items-center justify-center space-y-4 py-12">
<div className="relative">
<div className="w-16 h-16 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin"></div>
<div className="absolute inset-0 w-16 h-16 border-4 border-transparent border-b-pink-600 rounded-full animate-spin animate-reverse"></div>
</div>
<div className="text-center">
<p className="text-lg font-medium text-gray-700">Creating your image...</p>
<p className="text-sm text-gray-500">This may take up to 30 seconds</p>
</div>
</div>
);
}
Error Handling and Validation
Input Validation
Add comprehensive validation to your image generator:
const validatePrompt = (prompt: string): string | null => {
if (!prompt.trim()) {
return 'Please enter a description for your image';
}
if (prompt.length < 3) {
return 'Description must be at least 3 characters long';
}
if (prompt.length > 1000) {
return 'Description must be less than 1000 characters';
}
// Check for prohibited content (basic filtering)
const prohibitedWords = ['violence', 'explicit', 'harmful'];
const lowerPrompt = prompt.toLowerCase();
for (const word of prohibitedWords) {
if (lowerPrompt.includes(word)) {
return 'Please use appropriate language in your description';
}
}
return null;
};
Rate Limiting
Implement basic rate limiting in your API route:
// Add to your API route
const rateLimitMap = new Map();
function rateLimit(ip: string): boolean {
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 5; // 5 requests per minute
const requests = rateLimitMap.get(ip) || [];
const validRequests = requests.filter((time: number) => now - time < windowMs);
if (validRequests.length >= maxRequests) {
return false;
}
validRequests.push(now);
rateLimitMap.set(ip, validRequests);
return true;
}
Deployment
1. Prepare for Production
Update your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'oaidalleapiprodscus.blob.core.windows.net',
},
],
},
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
}
module.exports = nextConfig;
2. Deploy to Vercel
- Push your code to GitHub
- Connect your repository to Vercel
- Add your environment variables in Vercel dashboard
- Deploy!
npm run build
npm start
Performance Optimization
1. Image Optimization
Add Next.js Image component for better performance:
import Image from 'next/image';
// Replace img tags with:
<Image
src={image.url}
alt={image.prompt}
width={512}
height={512}
className="w-full h-full object-cover"
priority
/>
2. Caching Strategy
Implement caching for generated images:
// In your API route
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json(
{ message: 'Cached response' },
{
headers: {
'Cache-Control': 's-maxage=3600, stale-while-revalidate=86400',
},
}
);
}
Testing
Unit Tests
Create __tests__/ImageGenerator.test.tsx:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ImageGenerator from '../components/ImageGenerator';
// Mock fetch
global.fetch = jest.fn();
describe('ImageGenerator', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
test('renders prompt input', () => {
render(<ImageGenerator />);
expect(screen.getByPlaceholderText(/describe your image/i)).toBeInTheDocument();
});
test('shows error for empty prompt', async () => {
render(<ImageGenerator />);
const generateButton = screen.getByText(/generate image/i);
fireEvent.click(generateButton);
await waitFor(() => {
expect(screen.getByText(/please enter a prompt/i)).toBeInTheDocument();
});
});
});
Security Best Practices
1. API Key Security
- Never expose API keys in client-side code
- Use environment variables for all secrets
- Implement proper CORS settings
- Add request validation and sanitization
2. Content Filtering
// Add content filtering to your API route
function containsInappropriateContent(prompt: string): boolean {
const inappropriatePatterns = [
/violence/i,
/explicit/i,
// Add more patterns as needed
];
return inappropriatePatterns.some(pattern => pattern.test(prompt));
}
Conclusion
You’ve successfully built a complete AI image generator with Next.js! This application includes:
- Modern, responsive UI with Tailwind CSS
- Integration with OpenAI’s DALL-E API
- Image download functionality
- Error handling and validation
- Performance optimizations
- Security best practices
Next Steps
Consider adding these features to enhance your application:
- User Authentication – Allow users to save their images
- Image Editing – Basic editing tools like filters and cropping
- Social Sharing – Share generated images on social platforms
- Advanced AI Models – Support for multiple AI providers
- Batch Generation – Generate multiple images at once
- Custom Styles – Predefined artistic styles and filters
Resources
Happy coding! 🎨✨
