By the end, you’ll have a gorgeous, functional app that queries OpenAI and displays responses with flair. Let’s dive in!
We’ll start with a minimal Next.js app using TypeScript and Docker Compose.
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-dirSay "yes" to all prompts except src/ (we’re keeping it simple). This sets up Next.js with TypeScript and Tailwind CSS.
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_modulesStart the app:
docker-compose upVisit http://localhost:3000. You’ll see the default Next.js welcome page. Stop it with Ctrl+C when ready.
Let’s replace the default page with a simple, beautified "Hello World".
app/page.tsxEdit 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>
);
}docker-compose upRefresh http://localhost:3000. You’ll see a stunning gradient background with a pulsing "Hello, Next.js Hero!" message. Tailwind makes it irresistible already!
Let’s add a form to input a query for OpenAI.
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>
);
}docker-compose upVisit 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!
Now, let’s query OpenAI’s LLM. You’ll need an OpenAI API key (get one from platform.openai.com).
Stop Docker (Ctrl+C) and install the OpenAI package:
docker-compose run app npm install openaiCreate a .env.local file:
OPENAI_API_KEY=your-api-key-hereUpdate 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.localapp/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>
);
}.env.localEnsure NEXT_PUBLIC_OPENAI_API_KEY is set (prefix with NEXT_PUBLIC_ for client-side access):
NEXT_PUBLIC_OPENAI_API_KEY=your-api-key-heredocker-compose up --buildVisit 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).
Let’s add a loading spinner to make it irresistible.
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>
);
}docker-compose up --buildAsk a question, and watch the spinner animate while the AI responds. The UI is now smooth and professional!
app/api/ask/route.ts) for security.docker-compose up --buildVisit 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?
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.
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
DockerfileFROM 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.ymlversion: '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.tsxThis is your main frontend page.
"use client" to declare it's a client componentuseState tracks the question and responsehandleSubmit sends the question to /api/askapp/api/ask/route.tsxThis 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.tsxWraps all your pages in a common layout.
next/font<body> tagglobals.cssapp/globals.cssContains global CSS like font resets, Tailwind base, etc.
next.config.tsimport type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;✅ You can add experimental flags or custom Next.js settings here.
.envOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx✅ Put your OpenAI API key here (do not commit this to GitHub!)
postcss.config.mjsconst config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;✅ This sets up Tailwind's post-processing with PostCSS.
.env file:bash
echo 'OPENAI_API_KEY=your-key-here' > .env
docker compose up --buildhttp://localhost:3000Type your question and watch the magic! 🪄