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.
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-1generations 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.