0306090120150180210240270300330km/h
0 km/h
ADIT JANGID

Build a Full-Stack AI Chatbot with Next.js, LangChain, and OpenAI

A comprehensive, step-by-step guide to building a production-ready AI chatbot with Next.js (App Router), LangChain, and OpenAI, featuring streaming responses and chat history.

Build a Full-Stack AI Chatbot with Next.js, LangChain, and OpenAI

Conversational AI is transforming how we interact with web applications. Building a chatbot is no longer just about sending a prompt to an API and waiting for a static response. Modern web applications require real-time token streaming, conversational history (memory), and a responsive, accessible user interface.

In this tutorial, we will build a full-stack AI chatbot from scratch. We will use Next.js (App Router) for the framework, LangChain to orchestrate our LLM interactions and memory, and OpenAI's GPT-4o model to power the intelligence. To deliver an optimal user experience, we will implement streaming so that responses appear character-by-character as they are generated.


The Architecture

Before diving into the code, let’s look at the data flow of our application:

graph TD
    A[Client: React UI] -->|1. Send Message + History| B[Server: Next.js API Route]
    B -->|2. Formulate Prompt with Memory| C[LangChain Orchestrator]
    C -->|3. Request Stream| D[OpenAI API]
    D -->|4. Tokens Streamed| C
    C -->|5. Transform to Stream| B
    B -->|6. Stream Response| A
  1. Client (React UI): Users type a message. The UI appends it to the active conversation history and sends a POST request to our API endpoint.
  2. Server (Next.js Route Handler): Parses the request, instantiates LangChain's orchestrator, and handles environment credentials securely.
  3. LangChain & OpenAI: LangChain takes the chat history, constructs a structured prompt (combining system instructions and user history), and requests a stream from OpenAI.
  4. Streaming Response: The server forwards the stream chunks back to the client immediately, minimizing "Time to First Token" (TTFT).

Prerequisites & Dependencies

Make sure you have Node.js (v18.x or later) installed. Start a new Next.js project if you haven’t already:

npx create-next-app@latest ai-chatbot --typescript --tailwind --eslint
cd ai-chatbot

Install the required dependencies for LangChain, OpenAI, and handling streams:

npm install langchain @langchain/openai @langchain/core ai lucide-react
  • @langchain/openai: The official LangChain integration library for OpenAI models.
  • @langchain/core: Essential building blocks for LangChain (prompts, schemas, output parsers).
  • ai: The Vercel AI SDK, which contains utility helpers to stream responses seamlessly from Next.js endpoints to React components.
  • lucide-react: For modern UI icons.

Ensure you have your OpenAI API key ready. Create a .env.local file in your root directory:

OPENAI_API_KEY=your_openai_api_key_here

Step 1: Designing the System Prompt and Memory

We want our chatbot to have a specific persona and remember previous turns of the conversation. In LangChain, we construct a ChatPromptTemplate that includes a system message, placeholder variables for the message history, and the user's latest input.

Create a new file: app/api/chat/route.ts. This single Next.js Route Handler will process incoming POST requests, spin up the OpenAI client via LangChain, format the chat history, and stream the response back.

Write the following code in app/api/chat/route.ts:

import { NextRequest } from "next/server";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";

// Force edge runtime for lower latency streaming
export const runtime = "edge";

export async function POST(req: NextRequest) {
  try {
    const { messages } = await req.json();

    if (!messages || !Array.isArray(messages)) {
      return new Response(JSON.stringify({ error: "Messages array is required." }), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      });
    }

    // Convert client-side message representation into LangChain message instances
    const chatHistory = messages.map((m: { role: string; content: string }) => {
      if (m.role === "user") {
        return new HumanMessage(m.content);
      } else if (m.role === "assistant") {
        return new AIMessage(m.content);
      }
      return new SystemMessage(m.content);
    });

    // Extract the latest message
    const latestMessage = chatHistory[chatHistory.length - 1];
    // Remaining messages form the context memory
    const previousMessages = chatHistory.slice(0, -1);

    // Initialize OpenAI Model via LangChain
    const chatModel = new ChatOpenAI({
      modelName: "gpt-4o",
      temperature: 0.7,
      streaming: true,
    });

    // Create prompt template defining the chatbot's system persona and context window
    const prompt = ChatPromptTemplate.fromMessages([
      [
        "system",
        "You are a helpful, friendly, and knowledgeable software engineering assistant. " +
        "You provide clean, commented code examples, write clearly, and help debug issues logically.",
      ],
      new MessagesPlaceholder("history"),
      ["human", "{input}"],
    ]);

    // Construct the execution chain: Prompt -> Model -> Output Parser
    const parser = new StringOutputParser();
    const chain = prompt.pipe(chatModel).pipe(parser);

    // Invoke the chain and retrieve a readable stream
    const stream = await chain.stream({
      history: previousMessages,
      input: latestMessage.content,
    });

    // Convert LangChain stream to a standard Web ReadableStream compatible with Vercel AI SDK
    const webStream = new ReadableStream({
      async start(controller) {
        try {
          for await (const chunk of stream) {
            controller.enqueue(new TextEncoder().encode(chunk));
          }
          controller.close();
        } catch (err) {
          controller.error(err);
        }
      },
    });

    // Return the stream with appropriate Server-Sent Events headers
    return new Response(webStream, {
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Transfer-Encoding": "chunked",
      },
    });
  } catch (error: any) {
    console.error("Chat API Error:", error);
    return new Response(JSON.stringify({ error: error.message || "Internal Server Error" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

Step 2: Building the Client Chat Interface

Now, we need a beautiful, responsive user interface to interact with our streaming API. We will leverage the Vercel AI SDK's useChat hook, which manages input states, request loading states, message lists, and automatically handles SSE stream rendering.

Create or update your main page component. Replace app/page.tsx with the following content:

"use client";

import { useChat } from "ai/react";
import { Send, Bot, User, RotateCcw, Terminal } from "lucide-react";
import { useEffect, useRef } from "react";

export default function Home() {
  const { messages, input, handleInputChange, handleSubmit, setMessages, isLoading, error } =
    useChat({
      api: "/api/chat",
    });

  const chatContainerRef = useRef<HTMLDivElement>(null);

  // Automatically scroll to the bottom when new messages arrive or stream updates
  useEffect(() => {
    if (chatContainerRef.current) {
      chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
    }
  }, [messages]);

  const clearChat = () => {
    setMessages([]);
  };

  return (
    <main className="flex flex-col h-screen bg-slate-950 text-slate-100 font-sans">
      {/* Header */}
      <header className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-900/50 backdrop-blur-md">
        <div className="flex items-center gap-2">
          <Terminal className="w-6 h-6 text-teal-400" />
          <h1 className="text-xl font-bold tracking-tight">
            DevChat <span className="text-teal-400 text-xs px-2 py-1 rounded bg-teal-950 font-mono">GPT-4o</span>
          </h1>
        </div>
        <button
          onClick={clearChat}
          className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-slate-400 hover:text-white bg-slate-800 hover:bg-slate-700 rounded-md transition-all duration-200"
        >
          <RotateCcw className="w-3.5 h-3.5" />
          Reset Chat
        </button>
      </header>

      {/* Messages Window */}
      <div
        ref={chatContainerRef}
        className="flex-1 overflow-y-auto px-4 md:px-8 py-6 space-y-6 max-w-4xl mx-auto w-full scroll-smooth"
      >
        {messages.length === 0 ? (
          <div className="flex flex-col items-center justify-center h-full text-center space-y-4 max-w-md mx-auto">
            <div className="p-4 bg-teal-950/40 rounded-full border border-teal-900/30 text-teal-400">
              <Bot className="w-10 h-10" />
            </div>
            <h2 className="text-lg font-bold">Start a Conversation</h2>
            <p className="text-slate-400 text-sm leading-relaxed">
              Ask questions, debug issues, or brainstorm system architecture. The chatbot remembers context and streams code in real-time.
            </p>
          </div>
        ) : (
          messages.map((message) => (
            <div
              key={message.id}
              className={`flex gap-4 p-4 rounded-xl border ${
                message.role === "user"
                  ? "bg-slate-900/30 border-slate-800/50"
                  : "bg-slate-900/80 border-slate-800/80"
              }`}
            >
              <div
                className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
                  message.role === "user"
                    ? "bg-slate-800 text-slate-300"
                    : "bg-teal-950 text-teal-400 border border-teal-800/40"
                }`}
              >
                {message.role === "user" ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
              </div>
              <div className="flex-1 space-y-1.5 overflow-hidden">
                <span className="text-xs font-semibold text-slate-500 uppercase tracking-wider">
                  {message.role === "user" ? "You" : "Assistant"}
                </span>
                <p className="text-slate-300 leading-relaxed text-sm whitespace-pre-wrap selection:bg-teal-500 selection:text-black">
                  {message.content}
                </p>
              </div>
            </div>
          ))
        )}

        {isLoading && messages[messages.length - 1]?.role === "user" && (
          <div className="flex gap-4 p-4 rounded-xl bg-slate-900/40 border border-slate-800/50 animate-pulse">
            <div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-teal-400">
              <Bot className="w-4 h-4" />
            </div>
            <div className="flex-1 py-2 space-y-2">
              <div className="h-2.5 bg-slate-800 rounded-full w-12"></div>
              <div className="h-2.5 bg-slate-800 rounded-full w-2/3"></div>
            </div>
          </div>
        )}

        {error && (
          <div className="p-4 bg-red-950/30 border border-red-900/50 rounded-xl text-red-400 text-xs">
            Failed to stream response. Please verify your internet connection or API keys.
          </div>
        )}
      </div>

      {/* Input Form */}
      <footer className="border-t border-slate-800 bg-slate-900/30 backdrop-blur-md px-4 md:px-8 py-4">
        <form onSubmit={handleSubmit} className="max-w-4xl mx-auto flex gap-3 relative">
          <input
            type="text"
            value={input}
            onChange={handleInputChange}
            disabled={isLoading}
            placeholder="Type your coding query or request assistance..."
            className="flex-1 px-4 py-3 bg-slate-900 border border-slate-800 rounded-xl text-slate-200 placeholder:text-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent transition-all disabled:opacity-50"
          />
          <button
            type="submit"
            disabled={isLoading || !input.trim()}
            className="px-4 py-3 bg-teal-500 hover:bg-teal-400 disabled:bg-slate-800 disabled:text-slate-600 text-slate-950 font-semibold rounded-xl flex items-center gap-1.5 transition-all text-sm shrink-0 cursor-pointer shadow-lg shadow-teal-500/10"
          >
            <span className="hidden sm:inline">Send</span>
            <Send className="w-4 h-4" />
          </button>
        </form>
      </footer>
    </main>
  );
}

Step 3: Handling Complex Markdown Rendering

In the interface above, we render raw text as a string (message.content). However, software assistants usually write code blocks with syntax highlighting. Let's install two packages to easily render clean, formatted markdown and syntax-highlighted code inside our chats:

npm install react-markdown remark-gfm

Update the AI Assistant message representation inside the message loop:

import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

// Replace:
// <p className="text-slate-300 ...">{message.content}</p>
// With:
<div className="prose prose-invert prose-sm max-w-none text-slate-300 leading-relaxed">
  <ReactMarkdown remarkPlugins={[remarkGfm]}>
    {message.content}
  </ReactMarkdown>
</div>

(Tip: You can install @tailwindcss/typography to style markdown tags like p, ul, ol, and code blocks effortlessly using the prose utility).


Customizing System Persona and Memory Truncation

In a production setting, keeping all messages indefinitely can result in huge payloads and higher OpenAI token costs. You should implement a message truncation mechanism:

  1. Context Window Limitations: Restrict messages to the latest 10 messages before sending them to the API handler.
  2. System Prompt Optimization: Clearly instruct the LLM to format response outputs concisely.
  3. Caching: Store long-term context in a vector database like Pinecone or Supabase pgvector and query it via LangChain RAG vectors.

Conclusion

You have successfully built a full-stack AI chatbot application! Using Next.js, we safely handled our OpenAI API keys on the server and enabled low-latency Edge-runtime streaming. Using LangChain, we constructed a structured pipeline that pipes incoming prompt context, applies system templates, and translates LLM generators into readable client-side chunks. Finally, we engineered a modern, responsive chat console in Tailwind CSS.

You can deploy this application instantly on Vercel or any serverless provider capable of handling HTTP streams. The source code can easily be extended to support custom search engines, vector databases, or multiple model endpoints. Happy coding!

Next.jsLangChainOpenAIAITailwindCSS

Related Posts

Interested in working together?

Let's translate complex software and automation problems into clean, high-performance systems.