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
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:
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:
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:
'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:
npm install react-markdown remark-gfm
Update the message rendering to support markdown:
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:
npm install @upstash/ratelimit @upstash/redis
Update the API route:
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.