Build a Working AI Chatbot With Next.js and a Free LLM API Key (Zero Cost)
Full tutorial: build and deploy a streaming AI chatbot with Next.js 14 using a free OpenAI-compatible API key — includes frontend, API route, and Vercel deployment.
What We Are Building
By the end of this tutorial, you will have a working AI chatbot built with Next.js 14 that:
- Streams responses token by token (like ChatGPT)
- Maintains conversation history
- Lets users switch between models (GPT-4o, Claude, DeepSeek)
- Costs $0 to run (using FreeLLMKeys)
- Deploys to Vercel in one command
Project Setup
npx create-next-app@latest ai-chatbot --typescript --tailwind --app
cd ai-chatbot
npm install openai
Step 1 — Create the API Route
Create app/api/chat/route.ts:
import { NextRequest } from 'next/server'
import OpenAI from 'openai'
const client = new OpenAI({
baseURL: 'https://aiapiv2.pekpik.com/v1',
apiKey: process.env.FREELLMKEYS_API_KEY || 'sk-your-key-here',
})
export async function POST(req: NextRequest) {
const { messages, model = 'gpt-4o' } = await req.json()
const stream = await client.chat.completions.create({
model,
messages,
stream: true,
max_tokens: 1024,
})
const encoder = new TextEncoder()
const readable = 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(readable, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
},
})
}
Step 2 — Build the Chat UI
Replace app/page.tsx with:
'use client'
import { useState, useRef, useEffect } from 'react'
type Message = { role: 'user' | 'assistant'; content: string }
const MODELS = ['gpt-4o', 'deepseek-chat', 'claude-sonnet-4-6', 'gemini-2.5-flash']
export default function ChatPage() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const [model, setModel] = useState('gpt-4o')
const bottomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const send = async () => {
if (!input.trim() || loading) return
const userMsg: Message = { role: 'user', content: input }
const history = [...messages, userMsg]
setMessages(history)
setInput('')
setLoading(true)
// Add empty assistant message for streaming
setMessages([...history, { role: 'assistant', content: '' }])
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: history, model }),
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let assistantText = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
assistantText += decoder.decode(value, { stream: true })
setMessages([...history, { role: 'assistant', content: assistantText }])
}
setLoading(false)
}
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
{/* Model selector */}
<select
value={model}
onChange={(e) => setModel(e.target.value)}
className="mb-4 p-2 rounded border bg-gray-800 text-white"
>
{MODELS.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
{/* Messages */}
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((m, i) => (
<div
key={i}
className={`p-3 rounded-xl ${
m.role === 'user'
? 'bg-blue-600 ml-12 text-white'
: 'bg-gray-800 mr-12 text-gray-100'
}`}
>
<p className="text-sm whitespace-pre-wrap">{m.content}</p>
</div>
))}
{loading && messages[messages.length - 1]?.content === '' && (
<div className="bg-gray-800 mr-12 p-3 rounded-xl animate-pulse">
<div className="h-3 w-8 bg-gray-600 rounded" />
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && send()}
placeholder="Type a message…"
className="flex-1 p-3 rounded-xl border border-gray-700 bg-gray-800 text-white focus:outline-none"
/>
<button
onClick={send}
disabled={loading}
className="px-5 py-3 bg-blue-600 text-white rounded-xl disabled:opacity-50"
>
Send
</button>
</div>
</div>
)
}
Step 3 — Set Your API Key
Create a .env.local file:
FREELLMKEYS_API_KEY=sk-your-key-from-freellmkeys
Grab a fresh key from FreeLLMKeys.com and paste it here.
Step 4 — Run It
npm run dev
Open http://localhost:3000. You should see a working streaming chatbot with model selection.
Step 5 — Deploy to Vercel
npm i -g vercel
vercel
When prompted, add your environment variable: FREELLMKEYS_API_KEY=sk-your-key. Note: since FreeLLMKeys expire every 24–48 hours, you will need to update this env variable periodically in the Vercel dashboard or automate it.
Total Cost: $0
Next.js: free. Vercel hobby plan: free. FreeLLMKeys API: free. You now have a production-deployed streaming AI chatbot at zero monthly cost. Start from here and extend it with conversation memory, system prompt customization, and file uploads as your project grows.