← Back to Blog
Tutorial2025-12-278 min read

How to Add AI Image Generation to a Next.js App

A server-side API route that calls OpenAI's gpt-image-1, a client component that calls it, and the two mistakes (exposed keys, no error handling) that trip people up the first time.

By Kyle

How to Add AI Image Generation to a Next.js App

The short version: you need a server-side route that holds your OpenAI key and calls gpt-image-1, and a client component that calls that route — never the OpenAI API directly from the browser. Here's the whole thing, no filler.

Why a server route at all

If you call OpenAI's API straight from client-side JavaScript, your API key ships inside the bundle anyone can read in dev tools. That's not a theoretical risk — it's an immediate leak the moment your site goes live, and OpenAI keys billed to your account. The fix is a thin server route: the browser talks to your route, your route talks to OpenAI, and the key never leaves the server.

Step 1: The API route

App Router example, app/api/generate/route.js:

import OpenAI from "openai";
import { NextResponse } from "next/server";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function POST(request) {
  const { prompt } = await request.json();

  if (!prompt || typeof prompt !== "string") {
    return NextResponse.json({ error: "Missing prompt" }, { status: 400 });
  }

  try {
    const response = await openai.images.generate({
      model: "gpt-image-1",
      prompt,
      size: "1024x1024",
      quality: "medium",
    });

    const b64 = response.data[0].b64_json;
    return NextResponse.json({ image: `data:image/png;base64,${b64}` });
  } catch (err) {
    console.error("gpt-image-1 generation failed:", err);
    return NextResponse.json(
      { error: "Image generation failed" },
      { status: 500 }
    );
  }
}

If you're on the Pages Router instead, the same logic goes in pages/api/generate.js as a default-exported handler that checks req.method === 'POST' and reads req.body instead of await request.json() — the OpenAI call itself is identical.

OPENAI_API_KEY goes in .env.local and in your hosting provider's environment variables — never in code, never in a NEXT_PUBLIC_ variable (anything prefixed NEXT_PUBLIC_ gets shipped to the browser, which defeats the whole point).

Step 2: Call it from a client component

"use client";
import { useState } from "react";

export default function ImageForm() {
  const [prompt, setPrompt] = useState("");
  const [image, setImage] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  async function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const res = await fetch("/api/generate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
      });

      if (!res.ok) throw new Error("Request failed");

      const data = await res.json();
      setImage(data.image);
    } catch (err) {
      setError("Couldn't generate that image. Try again.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Describe an image..."
      />
      <button disabled={loading}>{loading ? "Generating..." : "Generate"}</button>
      {error && <p>{error}</p>}
      {image && <img src={image} alt={prompt} />}
    </form>
  );
}

That's the whole integration. No SDK magic, no hidden config — a fetch call to your own route, a loading flag, and an error message that doesn't just say "undefined."

Things that will actually bite you

  • Loading state isn't optional. gpt-image-1 generations take a few seconds, not milliseconds. Without a visible loading state, users double-click "Generate" and you get duplicate requests and duplicate charges.
  • Errors need a real message. OpenAI will reject some prompts (policy violations), rate-limit you under load, and occasionally time out. Catch all three and show something a non-technical user can act on — not a raw stack trace.
  • Cost adds up faster than people expect. A medium-quality 1024x1024 image is roughly $0.05. That's fine for a demo; at real traffic, it's a line item you need to track. OpenAI also offers a pricier gpt-image-2 tier with stronger prompt adherence if quality matters more than cost for a given feature — I'd default to gpt-image-1 at medium quality and only reach for the more expensive tier where it visibly earns its keep.
  • Rate limit on your side too. Even with the key safely server-side, nothing stops one user from hammering your route in a loop. Put a basic per-user or per-IP limit in front of it.

If you'd rather not run this yourself

Everything above is maybe 30 minutes of work the first time, but it's still a route to maintain, a key to rotate, and a cost dashboard to watch. If you just want a prompt box and a generated image without owning that infrastructure, that's the actual pitch for a managed service like Imagify — it's not cheaper per image than calling OpenAI directly, it's just zero setup. I go through the honest trade-offs in Imagify vs the OpenAI API directly, and if you're comparing providers beyond OpenAI, the API comparison covers quality, price, and speed across the current field.

Ready to Start Generating?

Try 2 free images as a guest, no account required. Create a free account to save and download them.

Get Started Free