Zero to Hero tutorial for Next.js, TypeScript, Tailwind CSS, and powered by an OpenAI LLM

By the end, you’ll have a gorgeous, functional app that queries OpenAI and displays responses with flair. Let’s dive in!


Step 1: Set Up the Foundation

We’ll start with a minimal Next.js app using TypeScript and Docker Compose.

1.1 Create Project Structure

Create a new directory called next-openai-hero and initialize it:

mkdir next-openai-hero
cd next-openai-hero
npx create-next-app@latest . --typescript --eslint --tailwind --app --no-src-dir

Say "yes" to all prompts except src/ (we’re keeping it simple). This sets up Next.js with TypeScript and Tailwind CSS.

1.2 Add Docker Compose

Create a Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

Create a docker-compose.yml:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules

1.3 Run It

Start the app:

docker-compose up

Visit http://localhost:3000. You’ll see the default Next.js welcome page. Stop it with Ctrl+C when ready.


Step 2: First Page with Tailwind

Let’s replace the default page with a simple, beautified "Hello World".

2.1 Update app/page.tsx

Edit app/page.tsx:

export default function Home() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-indigo-500 to-purple-600">
      <h1 className="text-4xl font-bold text-white animate-pulse">
        Hello, Next.js Hero!
      </h1>
    </div>
  );
}

2.2 Run It

docker-compose up

Refresh http://localhost:3000. You’ll see a stunning gradient background with a pulsing "Hello, Next.js Hero!" message. Tailwind makes it irresistible already!


Step 3: Add a Form

Let’s add a form to input a query for OpenAI.

3.1 Update app/page.tsx

"use client";

import { useState } from "react";

export default function Home() {
  const [query, setQuery] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    alert(`You entered: ${query}`);
    setQuery("");
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-indigo-500 to-purple-600">
      <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h1 className="text-3xl font-bold text-center text-indigo-600 mb-6">
          Ask the AI Hero
        </h1>
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Type your question..."
            className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 mb-4"
          />
          <button
            type="submit"
            className="w-full bg-indigo-600 text-white p-3 rounded-md hover:bg-indigo-700 transition-colors"
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  );
}

3.2 Run It

docker-compose up

Visit http://localhost:3000. You’ll see a sleek form. Type something, hit Submit, and get an alert with your input. The Tailwind styling makes it pop!


Step 4: Integrate OpenAI

Now, let’s query OpenAI’s LLM. You’ll need an OpenAI API key (get one from platform.openai.com).

4.1 Install OpenAI SDK

Stop Docker (Ctrl+C) and install the OpenAI package:

docker-compose run app npm install openai

4.2 Add Environment Variables

Create a .env.local file:

OPENAI_API_KEY=your-api-key-here

Update docker-compose.yml to load the .env.local:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    env_file:
      - .env.local

4.3 Update app/page.tsx

"use client";

import { useState } from "react";
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
  dangerouslyAllowBrowser: true, // Note: For demo only; use server-side in production
});

export default function Home() {
  const [query, setQuery] = useState("");
  const [response, setResponse] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const completion = await openai.chat.completions.create({
        model: "gpt-3.5-turbo",
        messages: [{ role: "user", content: query }],
      });
      setResponse(completion.choices[0].message.content || "No response");
    } catch (error) {
      setResponse("Error: Could not fetch response");
    }
    setQuery("");
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-indigo-500 to-purple-600">
      <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h1 className="text-3xl font-bold text-center text-indigo-600 mb-6">
          Ask the AI Hero
        </h1>
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Type your question..."
            className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 mb-4"
          />
          <button
            type="submit"
            className="w-full bg-indigo-600 text-white p-3 rounded-md hover:bg-indigo-700 transition-colors"
          >
            Ask AI
          </button>
        </form>
        {response && (
          <div className="mt-6 p-4 bg-gray-100 rounded-md">
            <p className="text-gray-800">{response}</p>
          </div>
        )}
      </div>
    </div>
  );
}

4.4 Update .env.local

Ensure NEXT_PUBLIC_OPENAI_API_KEY is set (prefix with NEXT_PUBLIC_ for client-side access):

NEXT_PUBLIC_OPENAI_API_KEY=your-api-key-here

4.5 Run It

docker-compose up --build

Visit http://localhost:3000. Type a question (e.g., "What’s the meaning of life?"), hit "Ask AI," and see the response beautifully displayed. Note: For production, move API calls to the server side (e.g., Next.js API routes).


Step 5: Polish with Loading State

Let’s add a loading spinner to make it irresistible.

5.1 Update app/page.tsx

"use client";

import { useState } from "react";
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
  dangerouslyAllowBrowser: true,
});

export default function Home() {
  const [query, setQuery] = useState("");
  const [response, setResponse] = useState("");
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    try {
      const completion = await openai.chat.completions.create({
        model: "gpt-3.5-turbo",
        messages: [{ role: "user", content: query }],
      });
      setResponse(completion.choices[0].message.content || "No response");
    } catch (error) {
      setResponse("Error: Could not fetch response");
    }
    setLoading(false);
    setQuery("");
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-indigo-500 to-purple-600">
      <div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h1 className="text-3xl font-bold text-center text-indigo-600 mb-6">
          Ask the AI Hero
        </h1>
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Type your question..."
            className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 mb-4"
            disabled={loading}
          />
          <button
            type="submit"
            className="w-full bg-indigo-600 text-white p-3 rounded-md hover:bg-indigo-700 transition-colors flex items-center justify-center"
            disabled={loading}
          >
            {loading ? (
              <svg
                className="animate-spin h-5 w-5 mr-2 text-white"
                viewBox="0 0 24 24"
              >
                <circle
                  className="opacity-25"
                  cx="12"
                  cy="12"
                  r="10"
                  stroke="currentColor"
                  strokeWidth="4"
                />
                <path
                  className="opacity-75"
                  fill="currentColor"
                  d="M4 12a8 8 0 018-8v8h8a8 8 0 01-16 0z"
                />
              </svg>
            ) : (
              "Ask AI"
            )}
          </button>
        </form>
        {response && (
          <div className="mt-6 p-4 bg-gray-100 rounded-md">
            <p className="text-gray-800">{response}</p>
          </div>
        )}
      </div>
    </div>
  );
}

5.2 Run It

docker-compose up --build

Ask a question, and watch the spinner animate while the AI responds. The UI is now smooth and professional!


Final Touches


Run the Final App

docker-compose up --build

Visit http://localhost:3000. You’ve gone from zero to hero! You’ve built a Next.js app with TypeScript, Tailwind CSS, and OpenAI integration—all in Docker Compose. It’s irresistible, functional, and ready to impress. What will you ask the AI next?

TUTORIAL

🚀 Beginner-Friendly Tutorial: Build an OpenAI-Powered App with Next.js 15 + Docker

This project shows you how to create a simple AI Q&A app using Next.js 15, TypeScript, Tailwind CSS, and OpenAI, all running inside Docker. It teaches full-stack app structure in a clean, beginner-friendly way.


🧠 What We’ll Build

A full-stack AI chatbot app with: - A simple form to enter a question - A backend that sends the question to OpenAI GPT-3.5 - A response shown back on the frontend - The whole thing runs securely inside Docker


📁 File-by-File Breakdown for Newbies

📄 Dockerfile

FROM node:18-alpine         # Start from a small Node.js image
WORKDIR /app                # Set working directory inside the container
COPY package*.json ./       # Copy dependency files first
RUN npm install             # Install npm packages
COPY . .                    # Copy the rest of the code
EXPOSE 3000                 # Expose port 3000
CMD ["npm", "run", "dev"]     # Run the development server

✅ This sets up your app in a container for isolation and consistency.


📄 docker-compose.yml

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    env_file:
      - .env

✅ This lets you start your whole app with a single command: docker compose up --build. ✅ It also loads .env to securely pass in your OpenAI API key.


📄 package.json

{
  "name": "next-openai-hero",
  "version": "0.1.0",
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "15.2.4",
    "openai": "^4.91.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "typescript": "^5",
    "tailwindcss": "^4"
  }
}

✅ This lists all the libraries you use and scripts you can run.


📄 app/page.tsx

This is your main frontend page.


📄 app/api/ask/route.tsx

This is your backend API route. It runs on the server.

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

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

export async function POST(req: NextRequest) {
  const { query } = await req.json();
  const completion = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: query }],
  });
  return NextResponse.json({ response: completion.choices[0].message.content });
}

✅ It gets the prompt from the request, calls OpenAI securely, and sends the response back.


📄 app/layout.tsx

Wraps all your pages in a common layout.


📄 app/globals.css

Contains global CSS like font resets, Tailwind base, etc.


📄 next.config.ts

import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  /* config options here */
};
export default nextConfig;

✅ You can add experimental flags or custom Next.js settings here.


📄 .env

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

✅ Put your OpenAI API key here (do not commit this to GitHub!)


📄 postcss.config.mjs

const config = {
  plugins: ["@tailwindcss/postcss"],
};
export default config;

✅ This sets up Tailwind's post-processing with PostCSS.


✅ How to Run the App

  1. Create your .env file:
  2. bash echo 'OPENAI_API_KEY=your-key-here' > .env
  3. Start the app:
docker compose up --build
  1. Open your browser:
http://localhost:3000

Type your question and watch the magic! 🪄