Back to Blog
TutorialsMay 25, 202510 min read

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.

F
FreeLLMKeys Team
Building tools for the AI developer community