Day 20 — Build a Streaming Chat App with Groq API
This is the final project day of the course.
You have built a RAG system (Day 9), a job search agent (Day 10), and an MCP server (Day 11). Today you build something different — a streaming chat application with conversation memory. Real-time responses that appear word by word, exactly like ChatGPT.
By end of day: a live URL. A chat app powered by Llama 3 running on Groq's hardware. Free to build, free to run.
Time required: 2 hours Cost: ₹0 (Groq free tier) Stack: Next.js + Groq API + Vercel
Why Groq for This Project
Groq runs at 500+ tokens per second — roughly 10x faster than OpenAI's GPT-4. This makes streaming feel dramatically better. Instead of watching text appear slowly, it streams almost as fast as you can read.
The Groq API is compatible with the OpenAI SDK. If you already know how to use OpenAI, you already know how to use Groq — just change the base URL.
The free tier gives you 14,400 requests per day. Enough to build, demo, share with classmates, and use for months without paying anything.
Before You Start
Get a Groq API key:
- Go to console.groq.com
- Sign up with Google or email
- Click "API Keys" → "Create API Key"
- Copy the key (starts with
gsk_)
No credit card required for the free tier.
Check your tools:
node --version # Need v18+
npm --version # Should work if Node is installed
git --version # For pushing to GitHub
Project Setup
Step 1: Create Next.js Project
npx create-next-app@latest groq-chat --typescript --tailwind --eslint --app --no-src-dir --import-alias "@/*"
cd groq-chat
Accept all defaults when prompted.
Step 2: Install Dependencies
npm install groq-sdk
That is the only dependency. Groq's SDK handles the API connection and streaming.
Step 3: Create .env.local
touch .env.local
Add:
GROQ_API_KEY=gsk_your_key_here
Step 4: Create .gitignore Entry
Open .gitignore and confirm .env.local is listed. It should be there by default in Next.js projects.
Project Structure
groq-chat/
├── .env.local ← Groq API key
├── app/
│ ├── api/
│ │ └── chat/
│ │ └── route.ts ← API route (streaming)
│ ├── page.tsx ← Chat UI
│ └── layout.tsx ← Root layout
└── package.json
File 1: The API Route
Create app/api/chat/route.ts:
// app/api/chat/route.ts
// Streaming chat API using Groq
import { NextRequest } from "next/server";
import Groq from "groq-sdk";
const groq = new Groq({
apiKey: process.env.GROQ_API_KEY,
});
export async function POST(req: NextRequest) {
try {
const { messages, systemPrompt } = await req.json();
if (!messages || !Array.isArray(messages)) {
return new Response("Invalid messages", { status: 400 });
}
// Build message history for Groq
const groqMessages = [
{
role: "system" as const,
content: systemPrompt || "You are a helpful AI assistant for engineering students preparing for placements in India. Be concise, practical, and encouraging.",
},
...messages.map((m: { role: string; content: string }) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
];
// Create streaming completion
const stream = await groq.chat.completions.create({
model: "llama-3.1-8b-instant", // Fast, free model
messages: groqMessages,
max_tokens: 1024,
temperature: 0.7,
stream: true, // Enable streaming
});
// Return as a readable stream
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
// Send each chunk as Server-Sent Event
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ content })}\n\n`)
);
}
}
// Signal end of stream
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
} catch (err) {
controller.error(err);
}
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
} catch (err: any) {
console.error("[chat API]", err?.message);
return new Response(
JSON.stringify({ error: err?.message || "Something went wrong" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
File 2: The Chat UI
Replace the contents of app/page.tsx with:
// app/page.tsx
// Streaming chat interface
"use client";
import { useState, useRef, useEffect } from "react";
interface Message {
role: "user" | "assistant";
content: string;
}
const SYSTEM_PROMPTS = [
{ label: "Placement Prep", value: "You are a placement preparation expert for Indian engineering students. Help with aptitude, technical interviews, HR rounds, and career guidance. Be specific and practical." },
{ label: "DSA Help", value: "You are a DSA (Data Structures & Algorithms) tutor. Help students understand concepts, solve problems, and prepare for technical interviews. Give examples in Python or Java." },
{ label: "Career Coach", value: "You are a career coach specialising in the Indian IT job market. Give honest, specific advice about roles, skills, salaries, and career paths for freshers." },
{ label: "General Assistant", value: "You are a helpful AI assistant. Answer questions clearly and concisely." },
];
export default function ChatPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [systemPrompt, setSystemPrompt] = useState(SYSTEM_PROMPTS[0].value);
const [selectedMode, setSelectedMode] = useState(0);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
const text = input.trim();
if (!text || loading) return;
// Add user message
const newMessages: Message[] = [...messages, { role: "user", content: text }];
setMessages(newMessages);
setInput("");
setLoading(true);
// Add empty assistant message that we will fill with streaming content
setMessages(prev => [...prev, { role: "assistant", content: "" }]);
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: newMessages,
systemPrompt: systemPrompt,
}),
});
if (!res.ok) throw new Error("API error");
if (!res.body) throw new Error("No response body");
// Read the stream
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6);
if (data === "[DONE]") break;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
// Append to the last message (the assistant's streaming response)
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
role: "assistant",
content: updated[updated.length - 1].content + parsed.content,
};
return updated;
});
}
} catch {
// Skip malformed chunks
}
}
}
} catch (err: any) {
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
role: "assistant",
content: "Sorry, something went wrong. Please try again.",
};
return updated;
});
} finally {
setLoading(false);
inputRef.current?.focus();
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
const clearChat = () => {
setMessages([]);
inputRef.current?.focus();
};
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center">
<span className="text-white text-sm font-bold">AI</span>
</div>
<div>
<p className="font-bold text-gray-900 text-sm">AI Career Assistant</p>
<p className="text-[10px] text-gray-400">Powered by Groq + Llama 3</p>
</div>
</div>
<button onClick={clearChat}
className="text-xs text-gray-400 hover:text-gray-600 border border-gray-200 px-3 py-1.5 rounded-lg transition-colors">
Clear chat
</button>
</header>
{/* Mode selector */}
<div className="bg-white border-b border-gray-100 px-4 py-2 flex gap-2 overflow-x-auto">
{SYSTEM_PROMPTS.map((prompt, i) => (
<button key={i}
onClick={() => { setSelectedMode(i); setSystemPrompt(prompt.value); }}
className={`text-xs font-semibold px-3 py-1.5 rounded-full whitespace-nowrap transition-colors ${
selectedMode === i
? "bg-violet-100 text-violet-700"
: "bg-gray-100 text-gray-500 hover:bg-gray-200"
}`}
>
{prompt.label}
</button>
))}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="text-4xl mb-4">🤖</div>
<p className="font-bold text-gray-700 mb-2">AI Career Assistant</p>
<p className="text-sm text-gray-400 max-w-sm">
Ask me anything about placements, DSA, career paths, or interview preparation.
</p>
<div className="mt-6 grid grid-cols-1 gap-2 w-full max-w-sm">
{[
"What should I focus on for TCS placement prep?",
"Explain binary search in simple terms",
"Should I learn DevOps or AI/ML first?",
"How to answer 'tell me about yourself' in HR?",
].map((suggestion, i) => (
<button key={i}
onClick={() => setInput(suggestion)}
className="text-left text-xs text-gray-600 bg-white border border-gray-200 px-3 py-2.5 rounded-xl hover:bg-violet-50 hover:border-violet-200 transition-colors">
{suggestion}
</button>
))}
</div>
</div>
)}
{messages.map((message, i) => (
<div key={i} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
{message.role === "assistant" && (
<div className="w-7 h-7 rounded-full bg-violet-100 flex items-center justify-center mr-2 mt-0.5 flex-shrink-0">
<span className="text-violet-700 text-xs font-bold">AI</span>
</div>
)}
<div className={`max-w-[80%] rounded-2xl px-4 py-3 ${
message.role === "user"
? "bg-violet-600 text-white rounded-br-sm"
: "bg-white border border-gray-100 text-gray-800 rounded-bl-sm shadow-sm"
}`}>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
{message.role === "assistant" && loading && i === messages.length - 1 && (
<span className="inline-block w-1.5 h-4 bg-violet-400 ml-0.5 animate-pulse rounded-sm" />
)}
</p>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="bg-white border-t border-gray-200 px-4 py-3">
<div className="flex items-end gap-2 max-w-3xl mx-auto">
<textarea
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask about placements, DSA, career paths..."
rows={1}
disabled={loading}
className="flex-1 resize-none border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-violet-300 disabled:opacity-50 max-h-32"
style={{ height: "auto" }}
onInput={e => {
const t = e.target as HTMLTextAreaElement;
t.style.height = "auto";
t.style.height = Math.min(t.scrollHeight, 128) + "px";
}}
/>
<button
onClick={sendMessage}
disabled={loading || !input.trim()}
className="w-10 h-10 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 text-white rounded-xl flex items-center justify-center flex-shrink-0 transition-colors"
>
{loading
? <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
}
</button>
</div>
<p className="text-center text-[10px] text-gray-400 mt-2">
Enter to send · Shift+Enter for new line · Powered by Groq (free)
</p>
</div>
</div>
);
}
Run It Locally
npm run dev
Open http://localhost:3000
You should see the chat interface. Try asking:
- "What should I focus on for Infosys placement?"
- "Explain the difference between stack and queue"
- "Should I learn AWS or Azure first?"
Responses stream in real time, word by word.
Common Errors and Fixes
Error: API key is required
Check that .env.local has GROQ_API_KEY=gsk_... with no spaces around =. Restart npm run dev after changing env files.
Module not found: groq-sdk
npm install groq-sdk
Responses not streaming (appear all at once)
The browser may be buffering. Check that the response headers include Content-Type: text/event-stream. In development with Next.js 14 this works correctly.
429 Too Many Requests
You hit Groq's rate limit. Wait 60 seconds. For sustained usage, use the llama-3.1-8b-instant model which has higher rate limits than larger models.
🔨 Break It — Learn by Experimenting
Experiment 1 — Change the Model
In route.ts, change the model:
model: "llama-3.3-70b-versatile" // Larger, smarter, slower
Compare the response quality to llama-3.1-8b-instant. Notice the difference in speed and quality.
Change back to the 8B model for normal use — it is fast enough for most tasks and uses less of your rate limit.
Experiment 2 — Change Temperature
Find temperature: 0.7 in route.ts.
Change to temperature: 0.1 and ask a creative question ("Write a poem about placement season").
Then change to temperature: 1.5 and ask the same question.
What you see: Low temperature = consistent, focused answers. High temperature = more creative, sometimes incoherent.
What you learned: Temperature controls randomness. For factual Q&A, keep it low (0.1-0.3). For creative tasks, increase it (0.7-1.0). Never go above 1.2 in production.
Experiment 3 — Remove Conversation History
In page.tsx, find where you send messages:
messages: newMessages,
Change to:
messages: [newMessages[newMessages.length - 1]], // Only current message
Have a multi-turn conversation. Notice the AI no longer remembers what you said before.
What you learned: LLMs are stateless. They have no memory between API calls. Conversation history is maintained by your application sending all previous messages with each request. This is why context windows matter — longer history = more tokens = higher cost.
Change it back to messages: newMessages.
🚀 The Challenge — Add a Typing Indicator
Currently when the AI is thinking (before the first token arrives), there is no visual indicator.
Add a typing indicator that appears between sending the message and receiving the first token:
[User message]
AI ●●● ← this, before first word arrives
Hint: Add a thinking state (separate from loading). Set it true when the request starts. Set it false when the first chunk arrives. Show a pulsing dots animation when thinking is true.
Deploy to Vercel
Step 1: Push to GitHub
git init
git add .
git commit -m "AI chat app with Groq streaming"
git branch -M main
git remote add origin https://github.com/yourusername/groq-chat.git
git push -u origin main
Step 2: Deploy on Vercel
- Go to vercel.com → New Project
- Import your
groq-chatrepository - Click "Deploy" — do not change any settings yet
Step 3: Add Environment Variable
After deployment:
- Go to your project on Vercel
- Settings → Environment Variables
- Add:
GROQ_API_KEY=gsk_your_key_here - Click Save
- Go to Deployments → click the three dots → Redeploy
Your app is now live. Share the URL.
What to Write in Your Portfolio
Project title: AI Streaming Chat Application
Description: Built a real-time streaming chat application using Next.js and Groq's LLM API. Implemented Server-Sent Events for streaming responses that appear word-by-word. Features conversation memory (full message history sent with each request), multiple AI personas via system prompts, and auto-scroll. Deployed on Vercel with environment-based API key management.
Tech stack: Next.js 14, TypeScript, Groq API, Llama 3, Vercel, Tailwind CSS
Live URL: your-app.vercel.app
How to Explain in an Interview
"I built a streaming chat application using Next.js and Groq's API. The key technical challenge was implementing streaming — instead of waiting for the complete response, the API returns chunks using Server-Sent Events, and the frontend reads the stream incrementally and appends each chunk to the UI. This gives the word-by-word effect you see in ChatGPT. I also implemented conversation memory by sending the full message history with each request, since LLMs are stateless — they have no built-in memory between calls. The app supports different system prompts which completely change the AI's persona and focus. I used Groq specifically because it runs Llama 3 at 500+ tokens per second, making the streaming feel almost instant."
You Have Now Completed 20 Days
Look at what you built:
| Day | Project |
|---|---|
| 8 | Personal portfolio website — live URL |
| 9 | RAG document Q&A system |
| 10 | Multi-step AI job search agent |
| 11 | GitHub MCP server for Claude Desktop |
| 20 | Streaming AI chat application |
Five real deployed projects. Not tutorial copies. Not "I followed a YouTube video." Real systems you built, broke, fixed, and deployed.
This is what separates you from the 90% of freshers who can only talk about what they plan to build.
Day 20 of 20 — AI Survival Kit Extended Edition Thank you for completing the course. Build something real. Get placed. Help the next batch.