AI Tutorials
May 5, 2026•4 min read•...
AI TutorialsMay 5, 2026•4 min read•

Build an AI Chat App with Next.js 15 and OpenAI

Learn to build a full-stack AI chat application using Next.js App Router, TypeScript, Tailwind CSS, and the OpenAI API with streaming support.

Build an AI Chat App with Next.js 15 and OpenAI

Build an AI Chat App with Next.js 15 and OpenAI

AI chat interfaces are everywhere — from customer support to coding assistants. In this tutorial, you’ll build a production-ready chat app with real-time streaming responses, message persistence, and a beautiful UI using Next.js 15, TypeScript, and Tailwind CSS.

Key Takeaways

  • Implement streaming responses with OpenAI’s Vercel AI SDK
  • Create a custom hook for chat state management
  • Add rate limiting and error handling
  • Deploy to Vercel with environment variables

Prerequisites

  • Node.js 20+ and npm/pnpm
  • OpenAI API key (get one at platform.openai.com)
  • Basic React and Next.js knowledge

Step 1: Project Setup

Create a new Next.js project with TypeScript and Tailwind:

bash
npx create-next-app@latest ai-chat-app --typescript --tailwind --app
cd ai-chat-app
npm install ai openai zod nanoid

The ai package (Vercel AI SDK) provides React hooks and streaming utilities.

Step 2: Create the API Route

Create app/api/chat/route.ts to handle OpenAI requests with streaming:

typescript
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { z } from 'zod';

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

export const runtime = 'edge';

const messageSchema = z.object({
  messages: z.array(
    z.object({
      role: z.enum(['user', 'assistant', 'system']),
      content: z.string(),
    })
  ),
});

export async function POST(req: Request) {
  try {
    const body = await req.json();
    const { messages } = messageSchema.parse(body);

    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini', // Fast and cheap
      stream: true,
      messages: [
        {
          role: 'system',
          content: 'You are a helpful AI assistant. Keep responses concise.',
        },
        ...messages,
      ],
      temperature: 0.7,
    });

    const stream = OpenAIStream(response);
    return new StreamingTextResponse(stream);
  } catch (error) {
    console.error(error);
    return new Response('Internal Server Error', { status: 500 });
  }
}

Step 3: Build the Chat UI Component

Create app/components/Chat.tsx:

tsx
'use client';

import { useChat } from 'ai/react';
import { useRef, useEffect } from 'react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api: '/api/chat',
      onError: (error) => {
        console.error('Chat error:', error);
      },
    });

  const messagesEndRef = useRef<HTMLDivElement>(null);

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

  return (
    <div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[80%] rounded-lg p-3 ${
                message.role === 'user'
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200 text-gray-900'
              }`}
            >
              {message.content}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-gray-200 rounded-lg p-3">
              <div className="typing-indicator">
                <span></span><span></span><span></span>
              </div>
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={input}
          onChange={handleInputChange}
          placeholder="Ask me anything..."
          className="flex-1 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading || !input.trim()}
          className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50"
        >
          Send
        </button>
      </form>
    </div>
  );
}

Step 4: Add Markdown and Code Highlighting

Install react-markdown and remark-gfm:

bash
npm install react-markdown remark-gfm

Update the message rendering to support markdown:

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

// Inside the message div:
<div className="prose prose-sm max-w-none">
  <ReactMarkdown remarkPlugins={[remarkGfm]}>
    {message.content}
  </ReactMarkdown>
</div>

Step 5: Rate Limiting and Production Hardening

Add rate limiting using Upstash Redis:

bash
npm install @upstash/ratelimit @upstash/redis

Update the API route:

typescript
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
  const { success } = await ratelimit.limit(ip);
  if (!success) {
    return new Response('Too many requests', { status: 429 });
  }
  // ... rest of the handler
}

Step 6: Deployment

Push to GitHub and deploy on Vercel. Add OPENAI_API_KEY to environment variables.

Performance Notes

  • Streaming reduces perceived latency (first token in <500ms)
  • Edge runtime enables global low-latency responses
  • Consider caching common prompts with Redis

Conclusion

You’ve built a fully functional AI chat app with streaming, markdown support, and rate limiting. Extend it with:

  • User authentication (NextAuth.js)
  • Chat history storage (PostgreSQL)
  • Image generation (DALL-E 3)
  • Voice input (Web Speech API)

Check out the live demo and GitHub repository for the complete source code.

Comments

Join the conversation — sign in to leave a comment.