DEV Community

Cover image for How to Build an AI Chatbot for Your Website with Next.js & OpenAI (2026 Guide)
Karen Dijital
Karen Dijital

Posted on

How to Build an AI Chatbot for Your Website with Next.js & OpenAI (2026 Guide)

Every website will have an AI assistant by the end of 2026. That's not a prediction — it's already happening. Businesses that don't adapt will feel like websites without mobile responsiveness in 2015.

But here's the thing: most tutorials show you a toy chatbot that nobody would actually use in production. No streaming, no context awareness, no rate limiting, no error handling.

In this guide, I'll show you how to build a production-ready AI chatbot for any website using Next.js and the OpenAI API. The same approach we use at Karen Dijital when building AI-powered solutions for our clients.

What we're building:

  • Streaming responses (ChatGPT-like typing effect)
  • System prompt customization (make the AI "know" your business)
  • Rate limiting (so your API bill doesn't explode)
  • Clean UI with Tailwind CSS
  • Production-ready error handling

Prerequisites

  • Node.js 20+
  • Basic Next.js knowledge (App Router)
  • An OpenAI API key (get one here)
  • 30 minutes of your time

Step 1: Project Setup

npx create-next-app@latest ai-chatbot --typescript --tailwind --app
cd ai-chatbot
npm install openai
Enter fullscreen mode Exit fullscreen mode

Create .env.local in the project root:

OPENAI_API_KEY=sk-your-api-key-here
Enter fullscreen mode Exit fullscreen mode

⚠️ Never expose your API key to the client. We'll call OpenAI from a server-side API route only. The frontend never sees the key.

Step 2: The API Route (Server-Side)

This is where the magic happens. We create a streaming API endpoint that:

  1. Receives the user's message
  2. Sends it to OpenAI with a system prompt
  3. Streams the response back in real-time
// src/app/api/chat/route.ts
import { OpenAI } from "openai";
import { NextRequest } from "next/server";

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

// Simple in-memory rate limiting
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT = 10; // max requests
const RATE_WINDOW = 60 * 1000; // per 60 seconds

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const record = rateLimitMap.get(ip);

  if (!record || now > record.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_WINDOW });
    return false;
  }

  record.count++;
  return record.count > RATE_LIMIT;
}

// 🔧 Customize this! Make the AI "know" your business
const SYSTEM_PROMPT = `You are a helpful assistant for a digital agency website.
You help visitors with questions about web development, e-commerce,
AI integration, and digital marketing services.

Rules:
- Be concise and helpful
- If asked about pricing, suggest they contact the team
- Always be professional but friendly
- Answer in the same language the user writes in`;

export async function POST(request: NextRequest) {
  try {
    const ip = request.headers.get("x-forwarded-for") || "unknown";

    if (isRateLimited(ip)) {
      return Response.json(
        { error: "Too many requests. Please wait a moment." },
        { status: 429 }
      );
    }

    const { messages } = await request.json();

    if (!messages || !Array.isArray(messages) || messages.length === 0) {
      return Response.json(
        { error: "Messages are required" },
        { status: 400 }
      );
    }

    // Limit conversation history to last 10 messages (save tokens)
    const recentMessages = messages.slice(-10);

    const stream = await openai.chat.completions.create({
      model: "gpt-4o-mini", // cheap & fast
      messages: [
        { role: "system", content: SYSTEM_PROMPT },
        ...recentMessages,
      ],
      stream: true,
      max_tokens: 500,
    });

    // Convert OpenAI stream to ReadableStream
    const encoder = new TextEncoder();
    const readableStream = new ReadableStream({
      async start(controller) {
        for await (const chunk of stream) {
          const text = chunk.choices[0]?.delta?.content || "";
          if (text) {
            controller.enqueue(encoder.encode(text));
          }
        }
        controller.close();
      },
    });

    return new Response(readableStream, {
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Cache-Control": "no-cache",
      },
    });
  } catch (error) {
    console.error("Chat API error:", error);
    return Response.json(
      { error: "Something went wrong. Please try again." },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this structure matters:

  • gpt-4o-mini — costs ~95% less than GPT-4o, fast enough for chat
  • Rate limiting — without this, one user could burn through your entire API budget
  • System prompt — this is how you make the AI "know" your business. Customize it!
  • Streaming — users see words appear in real-time instead of waiting 3-5 seconds for a full response
  • Message history limit — sending 100 messages to OpenAI costs 100x more. We cap at 10.

Step 3: The Chat Component (Frontend)

Now the fun part — the UI. This is a complete, production-ready chat component:

// src/components/ChatWidget.tsx
"use client";

import { useState, useRef, useEffect } from "react";

interface Message {
  role: "user" | "assistant";
  content: string;
}

export default function ChatWidget() {
  const [isOpen, setIsOpen] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  const sendMessage = async () => {
    if (!input.trim() || isLoading) return;

    const userMessage: Message = { role: "user", content: input.trim() };
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput("");
    setIsLoading(true);

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

      if (!res.ok) {
        const error = await res.json();
        throw new Error(error.error || "Request failed");
      }

      // Read the stream
      const reader = res.body?.getReader();
      const decoder = new TextDecoder();
      let assistantContent = "";

      // Add empty assistant message
      setMessages((prev) => [...prev, { role: "assistant", content: "" }]);

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        assistantContent += decoder.decode(value, { stream: true });

        // Update the last message with streamed content
        setMessages((prev) => {
          const updated = [...prev];
          updated[updated.length - 1] = {
            role: "assistant",
            content: assistantContent,
          };
          return updated;
        });
      }
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : "Something went wrong";
      setMessages((prev) => [
        ...prev,
        { role: "assistant", content: `Sorry, ${errMsg}` },
      ]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <>
      {/* Floating button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="fixed bottom-6 right-6 z-50 w-14 h-14 bg-blue-600
                   text-white rounded-full shadow-lg hover:bg-blue-700
                   transition-all flex items-center justify-center text-2xl"
      >
        {isOpen ? "" : "💬"}
      </button>

      {/* Chat window */}
      {isOpen && (
        <div className="fixed bottom-24 right-6 z-50 w-[380px] h-[500px]
                        bg-white rounded-2xl shadow-2xl border flex flex-col overflow-hidden">
          {/* Header */}
          <div className="bg-blue-600 text-white px-5 py-4">
            <h3 className="font-semibold">AI Assistant</h3>
            <p className="text-blue-100 text-xs">Ask me anything</p>
          </div>

          {/* Messages */}
          <div className="flex-1 overflow-y-auto p-4 space-y-3">
            {messages.length === 0 && (
              <p className="text-gray-400 text-sm text-center mt-8">
                👋 Hi! How can I help you today?
              </p>
            )}
            {messages.map((msg, i) => (
              <div
                key={i}
                className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
              >
                <div
                  className={`max-w-[80%] px-4 py-2.5 rounded-2xl text-sm leading-relaxed ${
                    msg.role === "user"
                      ? "bg-blue-600 text-white rounded-br-md"
                      : "bg-gray-100 text-gray-800 rounded-bl-md"
                  }`}
                >
                  {msg.content}
                  {msg.role === "assistant" && isLoading && i === messages.length - 1 && (
                    <span className="inline-block w-1.5 h-4 bg-gray-400 ml-1 animate-pulse" />
                  )}
                </div>
              </div>
            ))}
            <div ref={messagesEndRef} />
          </div>

          {/* Input */}
          <div className="border-t p-3">
            <div className="flex gap-2">
              <input
                type="text"
                value={input}
                onChange={(e) => setInput(e.target.value)}
                onKeyDown={(e) => e.key === "Enter" && sendMessage()}
                placeholder="Type a message..."
                className="flex-1 px-4 py-2.5 rounded-xl bg-gray-100 text-sm
                           outline-none focus:ring-2 focus:ring-blue-300"
                disabled={isLoading}
              />
              <button
                onClick={sendMessage}
                disabled={isLoading || !input.trim()}
                className="px-4 py-2.5 bg-blue-600 text-white rounded-xl text-sm
                           font-medium hover:bg-blue-700 disabled:opacity-50
                           disabled:cursor-not-allowed transition-colors"
              >
                Send
              </button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Add It to Your Layout

Drop it into your root layout and it appears on every page:

// src/app/layout.tsx
import ChatWidget from "@/components/ChatWidget";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <ChatWidget />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's it. Run npm run dev, open localhost:3000, click the chat bubble.

The Cost Reality

Everyone asks: "How much will this cost to run?"

Scenario Monthly Messages Model Estimated Cost
Small blog 500 gpt-4o-mini ~$0.30
Business site 5,000 gpt-4o-mini ~$3.00
High-traffic 50,000 gpt-4o-mini ~$30.00
High-traffic 50,000 gpt-4o ~$300.00

gpt-4o-mini is the sweet spot for chatbots. It's smart enough for 95% of customer questions and costs almost nothing. Only upgrade to gpt-4o if you need complex reasoning.

Our rate limiter (10 requests per minute per IP) protects against abuse. For extra safety, add a monthly budget cap in your OpenAI dashboard.

Taking It to Production: 5 Pro Tips

1. Make the AI Actually Useful

The system prompt is everything. A generic "you are a helpful assistant" is useless. Be specific:

const SYSTEM_PROMPT = `You are the AI assistant for TechCorp,
a SaaS company that sells project management tools.

You know:
- Pricing: Starter ($9/mo), Pro ($29/mo), Enterprise (custom)
- Free trial: 14 days, no credit card required
- Main features: Kanban boards, time tracking, team chat
- Support hours: Mon-Fri 9-18 (UTC+3)

Rules:
- If someone asks to buy, send them to /pricing
- For technical issues, suggest they email support@techcorp.com
- Never make up features that don't exist
- Be concise — max 2-3 sentences per response`;
Enter fullscreen mode Exit fullscreen mode

2. Add Conversation Starters

Help users know what to ask:

const starters = [
  "What services do you offer?",
  "How much does it cost?",
  "I need help with my project",
];
Enter fullscreen mode Exit fullscreen mode

3. Save Conversations (Optional)

If you want analytics on what users ask:

// In your API route, after the stream completes:
await fetch("/api/log-chat", {
  method: "POST",
  body: JSON.stringify({
    messages: recentMessages,
    timestamp: new Date().toISOString(),
    ip: ip, // anonymize this!
  }),
});
Enter fullscreen mode Exit fullscreen mode

4. Add a Disclaimer

AI can hallucinate. Protect yourself:

<p className="text-gray-400 text-[10px] text-center px-4 py-1">
  AI-generated responses may not always be accurate.
  For critical matters, please contact us directly.
</p>
Enter fullscreen mode Exit fullscreen mode

5. Respect User Privacy

  • Don't log personal information from chats
  • Add the chatbot to your privacy policy
  • Consider GDPR/KVKK compliance if operating in EU/Turkey
  • Use the rate limiter to prevent data scraping

The Result

In about 30 minutes, you now have:

✅ A streaming AI chatbot on your website
✅ Rate limiting to control costs
✅ Customizable system prompt for your business
✅ Clean, responsive UI
✅ Production-ready error handling

The total code is ~200 lines. No third-party chat SDKs, no monthly subscriptions, no vendor lock-in.

When NOT to Build It Yourself

Self-hosting isn't always the answer. Here's when to consider alternatives:

Situation Better Option
Need analytics dashboard Intercom, Crisp, Tidio
Enterprise compliance (SOC2, HIPAA) Azure OpenAI Service
No developer on team Chatbase, CustomGPT
Need voice + chat Vapi, ElevenLabs

But if you want full control, zero monthly fees, and custom behavior — the approach in this article is the way to go.

What's Next?

Some ideas to extend this:

  • RAG (Retrieval Augmented Generation) — feed your website content to the AI so it can answer questions about your actual products/services
  • Multi-language support — detect the user's language and respond accordingly
  • Handoff to human — if the AI can't answer, escalate to a real support agent
  • Analytics — track common questions to improve your FAQ and content

If you want to see AI chatbots and other AI integrations in action on real business websites, check out the AI integration projects and chatbot solutions we build at Karen Dijital.

If this was helpful, drop a ❤️ and follow for more practical AI + web dev guides.

I'm building at Karen Dijital — a digital agency in Istanbul focused on web development, AI integration, and process automation. We write about what we build.

Top comments (0)