0 Comments

Listen to this article

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

  1. Push your code to GitHub
  2. Connect your repository to Vercel
  3. Add your environment variables in Vercel dashboard
  4. 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:

  1. User Authentication – Allow users to save their images
  2. Image Editing – Basic editing tools like filters and cropping
  3. Social Sharing – Share generated images on social platforms
  4. Advanced AI Models – Support for multiple AI providers
  5. Batch Generation – Generate multiple images at once
  6. Custom Styles – Predefined artistic styles and filters

Resources

Happy coding! 🎨✨

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts