Large Language ModelsGenerate imagesGenerate videos

Create an AI Image Generator Website: Template and GitHub Project

A working template for an AI image generator website: the Next.js stack, the GitHub project layout, the server route that calls an image model API, the prompt page, safety checks, rate limits and deployment. Every file is shown, so you can copy it and ship.

Create an AI Image Generator Website: Template and GitHub Project
Cristian Da Conceicao
Founder of Picasso IA

Most tutorials on how to create an AI image generator website end with a screenshot and a vague promise. This one ends with a working project: a prompt box, a server route that calls an image model, a polling loop, a result view and a deploy checklist. Every file fits on this page, so you can paste it into a fresh repository, push it to GitHub and have a live generator the same afternoon.

The template is small on purpose. It has four files of real logic, no database, no login provider and no state library. That makes it easy to read in one sitting and easy to extend later with accounts, history or video. If you can run npm install, you can ship it.

💡 Quick answer: an image generator website is a form, one server route that hides your API token, a polling loop and an <img> tag. Everything else is polish.

What the Template Does

The finished site takes a text prompt, sends it to an image model, waits for the result and shows the picture with a download link. The browser never sees your API token, because every call goes through your own server route.

Here is the full request flow:

  1. The visitor types a prompt and presses the button.
  2. The page posts the prompt to /api/generate on your server.
  3. Your server validates it and creates a job on the image API.
  4. The page asks /api/generate/{id} for the status every two seconds.
  5. When the status turns to succeeded, the page shows the image URL.

Whiteboard sketch of the browser, server and image model architecture

Features at a glance

FeatureIncludedWhere it lives
Prompt form with character limitYescomponents/Generator.tsx
Server route that creates the jobYesapp/api/generate/route.ts
Status route for pollingYesapp/api/generate/[id]/route.ts
Loading, error and timeout statesYescomponents/Generator.tsx
Download link and local historyYesAdded in the gallery step
Accounts, billing, user galleriesNoAdd after the first launch

Who it fits: solo developers get a portfolio piece that actually generates pictures. Agencies get a base they can brand for a client in a day. Product teams get a prototype to test demand before they invest in a full platform.

Stack and Project Layout

Laptop in a cafe showing a simple image generator page with a prompt field

Choose the stack

The template uses Next.js with the App Router, TypeScript and Tailwind CSS. One framework gives you the page and the server routes in the same repository, which is why it suits a first project. Any stack with a server works the same way: React with Express, SvelteKit, Nuxt or plain Node.

Create the project and put it on GitHub in four commands:

npx create-next-app@latest ai-image-generator --ts --app --tailwind --eslint
cd ai-image-generator
git init && git add . && git commit -m "Initial template"
gh repo create ai-image-generator --public --source=. --push

The backend is a text to image API. The PicassoIA API follows the Replicate convention: POST /v1/models/{owner}/{name}/predictions creates a job, and GET /v1/predictions/{id} returns its status. At the time of writing, the API serves four models: picassoia/picassoia-image, picassoia/picassoia-image-editor-pro, picassoia/picassoia-video and picassoia/seedance-2.5-lite. Create your token on the PicassoIA API page.

Folder tree and variables

ai-image-generator/
├── app/
│   ├── api/
│   │   └── generate/
│   │       ├── route.ts          # creates the prediction
│   │       └── [id]/route.ts     # returns status and output
│   ├── layout.tsx
│   └── page.tsx                  # renders <Generator />
├── components/
│   └── Generator.tsx             # form, polling, result
├── .env.example
├── .gitignore
└── README.md

Top-down view of a desk with a wireframe, laptop and phone showing a photo gallery

Two environment variables drive everything:

# .env.example
PICASSOIA_API_TOKEN=pia_sk_replace_me
PICASSOIA_MODEL=picassoia/picassoia-image

Copy the file to .env.local and paste your real token there. create-next-app ignores every .env* file, so add the line !.env.example to .gitignore if you want the example file in the repository. Never put the token in a variable that starts with NEXT_PUBLIC_, because Next.js ships those to the browser.

Write the Server Route

Developer hands typing code on a laptop in front of a monitor

Create the prediction

// app/api/generate/route.ts
import { NextResponse } from "next/server";

const API = "https://api.picassoia.com/v1";
const MODEL = process.env.PICASSOIA_MODEL ?? "picassoia/picassoia-image";

export async function POST(req: Request) {
  const { prompt } = await req.json();
  if (typeof prompt !== "string" || prompt.length < 3 || prompt.length > 4000) {
    return NextResponse.json({ error: "Invalid prompt" }, { status: 400 });
  }

  const res = await fetch(`${API}/models/${MODEL}/predictions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ input: { prompt } }),
  });

  if (!res.ok) {
    return NextResponse.json({ error: "Upstream error" }, { status: 502 });
  }
  const prediction = await res.json();
  return NextResponse.json({ id: prediction.id });
}

The body follows the Replicate convention, an input object that holds the prompt. Each model page lists extra fields, such as aspect ratio, that you can add next to prompt. The 4,000 character check matches the documented prompt limit, so users get a clean error before the request leaves your server.

Poll until it finishes

Generation is asynchronous. The first call returns an id, and you ask for the status until it says succeeded or failed.

// app/api/generate/[id]/route.ts
import { NextResponse } from "next/server";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  if (!/^[A-Za-z0-9_-]+$/.test(id)) {
    return NextResponse.json({ error: "Bad id" }, { status: 400 });
  }

  const res = await fetch(`https://api.picassoia.com/v1/predictions/${id}`, {
    headers: { Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}` },
    cache: "no-store",
  });
  const p = await res.json();
  const output = Array.isArray(p.output) ? p.output[0] : p.output;
  return NextResponse.json({
    status: p.status,
    output: output ?? null,
    error: p.error ?? null,
  });
}

The id check matters. Without it, a visitor could pass ../ segments and make your server call a different endpoint with your token attached.

Handle errors and limits

Three failure types show up in real traffic:

  1. Bad input: return 400 before you spend a request.
  2. Upstream errors or busy queues: return 502 or 429 and let the page show a retry message.
  3. Slow jobs: stop polling after about two minutes and tell the user.

The API allows 5 concurrent predictions per account, shared across every token. If your site gets ten visitors at once, five wait. Add a small queue or disable the button while a job runs.

SymptomLikely causeFix
401 from the APIWrong or missing tokenCheck .env.local and restart npm run dev
429 or long waitsAll 5 slots are busyQueue requests or show a wait message
Status never changesPolling the wrong idLog the id returned by the first call
Works locally, fails onlineVariables not set on the hostAdd both variables in the host settings

Build the Prompt Page

Designer sketching a card layout for an image gallery on a tablet

Form and state

// components/Generator.tsx
"use client";
import { useState } from "react";

export default function Generator() {
  const [prompt, setPrompt] = useState("");
  const [image, setImage] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function generate() {
    setBusy(true);
    setError(null);
    setImage(null);
    try {
      const start = await fetch("/api/generate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
      });
      if (!start.ok) throw new Error("Could not start the job");
      const { id } = await start.json();

      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 2000));
        const poll = await (await fetch(`/api/generate/${id}`)).json();
        if (poll.status === "succeeded") return setImage(poll.output);
        if (poll.status === "failed" || poll.status === "canceled") {
          throw new Error(poll.error ?? "Generation failed");
        }
      }
      throw new Error("Timed out, try again");
    } catch (e) {
      setError((e as Error).message);
    } finally {
      setBusy(false);
    }
  }

  return (
    <main className="mx-auto max-w-3xl p-6">
      <textarea
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        maxLength={4000}
        placeholder="A lighthouse at dawn, 35mm film, soft fog"
        className="w-full rounded border p-3"
      />
      <button
        onClick={generate}
        disabled={busy || prompt.length < 3}
        className="mt-3 rounded bg-black px-4 py-2 text-white disabled:opacity-50"
      >
        {busy ? "Generating..." : "Generate"}
      </button>
      {error && <p className="mt-3 text-red-600">{error}</p>}
      {image && <img src={image} alt={prompt} className="mt-6 w-full rounded" />}
    </main>
  );
}

Import it in app/page.tsx and render <Generator />. Run npm run dev, open localhost:3000, type a prompt and you have a working generator.

Gallery and downloads

Add two touches once the basics work. Wrap the image in a link with the download attribute so users can save the file. Then store each result in localStorage, an array of { prompt, url } objects, and render it below the form as a grid. That gives you history without a database.

💡 Keep the prompt as the alt text. It helps screen readers, and it gives your gallery useful text for search engines.

How to Use Imagen 4 on PicassoIA

Before you hard-code a look into your site, test it where iteration is cheap. The PicassoIA website has a playground for every model, and the controls follow the same pattern: a prompt field, a few settings and a generate button.

Printed photographs laid out in a grid beside a laptop showing a gallery page

Run a test prompt

  1. Open the Imagen 4 page.
  2. Paste a structured prompt: subject, setting, light, lens and texture.
  3. Choose a 16:9 aspect ratio if the model offers it.
  4. Press generate and check the result at full size.
  5. Change one detail at a time and generate again.

A prompt that works well for photographic output:

A ceramic cup of black coffee on a worn oak table, window light from the left, 50mm lens at f/2, shallow depth of field, visible wood grain and steam, Kodak Portra 400 colors.

Each part of that prompt does a job:

PartExampleWhy it helps
SubjectA ceramic cup of black coffeeNames the one thing the picture is about
SettingA worn oak tableGives the background a material and a mood
LightWindow light from the leftSets shadows and direction
Lens50mm at f/2Controls depth of field and perspective
TextureWood grain, steamPushes the result toward a real photograph

Compare three models

Run the same prompt through several models before you decide what powers the site. The API serves only the four picassoia/* models at the time of writing, so the others in this table are for choosing a look in the playground.

ModelStrengthPage
Imagen 4Natural photographic detailOpen
FLUX 2 ProStrong prompt followingOpen
Seedream 4.5Rich color and portraitsOpen
GPT Image 2Text inside imagesOpen
FLUX SchnellFast draftsOpen
PicassoIA ImageThe default model in the templateOpen

Keep the winning prompt as your starter text in the placeholder attribute, and turn your best three prompts into clickable example chips under the textarea. Visitors who see a good example write better prompts, and better prompts mean fewer wasted generations.

Add LLMs and Video Later

Draft code with an LLM

You can build the whole template above with a coding assistant. Claude Sonnet 5 and GPT 5.6 Sol are both listed for coding tasks, and Gemini 3.5 Flash suits quick edits. Paste in this article's folder tree and ask for one file at a time, then read every line before you commit it.

Two developers pair programming on a web app with a grid of generated pictures

Rewrite short prompts

Most visitors type five words. A language model can expand them before the image call:

  1. Add a rewrite step in route.ts that sends the user's text to a model such as Kimi K2.6.
  2. Instruct it to return one prompt with subject, light, lens and texture.
  3. Send that prompt to the image model and show both versions to the user.

Animate the results

The same two routes handle video. Change PICASSOIA_MODEL to a video model, such as picassoia/seedance-2.5-lite, and the output becomes an MP4 link. Render it with a <video controls> tag and raise the polling interval, because video takes longer than images. To compare options first, the Seedance 2.5 Lite page and the Wan 3 page show what each model produces.

Ship Without Surprises

Server racks in a quiet data center aisle

Moderate prompts

A public generator gets abused within days. Run each prompt through a moderation model before you create the prediction. Llama Guard 4 12B is built for that job: it labels a prompt as safe or unsafe, and you block the request when it says unsafe. Add a visible terms notice under the form too.

Rate limits and queues

RiskFix
One visitor floods the routeLimit requests per IP, for example 10 per hour
More visitors than concurrent slotsDisable the button while a job runs and show a wait message
Token leaks in the browserKeep calls on the server and never use NEXT_PUBLIC_ for secrets
Runaway costCap daily generations and alert when you reach 80%

An in-memory counter works on a single server. On serverless hosting, every function instance has its own memory, so use a shared store for the limiter.

Deploy and monitor

Deploying takes five steps:

  1. Push the repository to GitHub.
  2. Import it into your hosting platform of choice.
  3. Add PICASSOIA_API_TOKEN and PICASSOIA_MODEL as environment variables.
  4. Set a maximum function duration long enough for your polling route.
  5. Open the live URL and generate three test images.

After launch, log the status of each job (never the token) and check the failure rate weekly. A sudden rise usually means a model change or a limit you hit. Write a short README.md too, with the two variables, the run command and a screenshot, because the README is the first thing people see on a GitHub project.

Build Yours on Picasso IA

You now have the pieces: a stack, a folder tree, two server routes, a prompt page and a safety checklist. The fastest way to make the site feel finished is to pick its look before you write more code.

Small team celebrating the launch of their image generator website

Open Picasso IA, run your first prompt through Imagen 4 or FLUX 2 Pro, and save the three results you like most. Those prompts become your example chips, your landing page and your first test cases. Then create your API token on the PicassoIA API page, paste the code from this article into a new repository and press generate. The first image on your own domain is the moment the project becomes real, so make that prompt a good one.

Share this article