Build a Chatbot with Google Gemini

Build a web chatbot that connects to Google Gemini with multi-turn conversations.

Introduction

30 Second Summary

AI chatbots have become part of daily life. From answering questions to helping write emails, they seem to understand exactly what you need.

In this project, you will build your own chatbot web app powered by Google Gemini. You'll wire up a chat interface that sends messages to Google's AI and displays responses in a real-time conversation.

What You'll Build

A full-stack chatbot application with a Node.js backend server and a polished browser-based chat interface that communicates with the Google Gemini API.

By the end of this project, you'll have:

  • A working Node.js backend that authenticates with and calls the Google Gemini API.
  • A polished chat interface built with HTML, CSS, and JavaScript.
  • Support for multi-turn conversations so your chatbot remembers what you said earlier.
  • Secret Mission: Add streaming responses so messages appear word-by-word like a real AI chat experience.

Are there any prerequisites?

None. This project includes full setup instructions for every tool you need. You don't need any prior coding experience or accounts set up beforehand.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can build your chatbot, you need a place to write code and a way to run JavaScript outside the browser.

In this step, you will install Node.js (a JavaScript runtime) and VS Code (a code editor), then create your project folder with its starting files.

In this step, get ready to:
  • Install Node.js to run JavaScript on your computer.
  • Install VS Code for editing your project files.
  • Create and initialize your project folder with its dependencies.
Install Node.js

Node.js lets you run JavaScript on your computer instead of only inside a browser. It also comes with npm, a package manager that downloads libraries for your project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node --version

Look at the output and pick the tab below that matches what you see.

✔️ I see version 18 or higher

You see a version number like v24.16.0 or higher. Node.js is ready to go.

  • Confirm npm is also available by running:
npm --version

You should see a version number like 10.x or higher. Both tools are ready.

ⓧ I see a version lower than 18

Your installed version is too old for this project. The Google GenAI SDK requires Node.js v18 or higher.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the LTS version (currently v24.16.0).
  • Run the installer and accept the default settings.
  • Close your terminal completely, then reopen it.
  • Run node --version again to confirm it now shows v18 or higher.

Why close and reopen the terminal?

Your terminal loads its available commands when it first starts. After installing Node.js, the terminal needs a fresh start to recognize the new node and npm commands.

ⓧ Command not found

Node.js is not installed on your computer yet. Let's install it now.

  • Go to https://nodejs.org/en/download in your browser.
  • Download the LTS version (currently v24.16.0).
  • Run the installer and accept the default settings.
  • Close your terminal completely, then reopen it.
  • Run node --version to confirm it shows v18 or higher.
  • Run npm --version to confirm npm is also available.

Still seeing command not found?

Make sure you fully closed and reopened your terminal after installing. On macOS, check that you opened a new Terminal window (not just a new tab in the same window). On Windows, the installer may ask you to restart your computer.

Help me troubleshoot Node.js not being recognized after installation.

What is Node.js?

Node.js is a runtime that lets you execute JavaScript code directly on your computer. Without it, JavaScript can only run inside a web browser.

For this project, Node.js powers the backend server that talks to the Gemini API. It also gives you npm for downloading packages other developers have built.

Install VS Code and create your project folder

VS Code is a free code editor that gives you syntax highlighting, a built-in terminal, and a file explorer all in one window.

  • Go to https://code.visualstudio.com in your browser.
  • Click the download button for your operating system (macOS or Windows).
  • Run the installer and follow the prompts to complete the installation.

✔️ I already have VS Code installed

Great. Skip ahead to the next bullet below where you open your project folder.

ⓧ I just installed it

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

Now create a dedicated folder for your chatbot project.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir gemini-chatbot
cd gemini-chatbot
  • Open this folder in VS Code by running:
code .

VS Code reopens with gemini-chatbot as the active workspace. You should see the folder name in the top of the window and an empty Explorer sidebar on the left.

code command not recognized?

On macOS, open the Command Palette in VS Code with Cmd+Shift+P, type shell command, and select Shell Command: Install 'code' command in PATH. Then restart your terminal and try again.

On Windows, the VS Code installer adds code to your PATH automatically. If it still does not work, restart your terminal or computer.

Help me get the code command working in my terminal.

Initialize your project and install dependencies

Every Node.js project starts with a package.json file that tracks your project's name, version, and dependencies.

  • Open a new terminal in VS Code by pressing Ctrl+` (if the terminal is not already open).
  • Confirm you are inside the gemini-chatbot folder. The terminal prompt should show gemini-chatbot at the end.
  • Scaffold a default package.json by running:
npm init -y

You should see output confirming a package.json file was created. You will also see it appear in the Explorer sidebar.

What does npm init -y do?

This command creates a package.json with sensible defaults. The -y flag means "yes to all prompts" so you skip the interactive questionnaire.

The default file needs a few edits to match your project. Replace its contents with the correct metadata.

  • Click on package.json in the Explorer sidebar to open it.
  • Select all the text with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Paste the following code into the file:
{
  "name": "gemini-chatbot",
  "version": "1.0.0",
  "description": "A chatbot powered by Google Gemini",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  }
}

What do these fields mean?

  • "name" identifies your project. It matches the folder name.
  • "main" tells Node.js which file to run first. You will create server.js in a later step.
  • "scripts" defines shortcuts. Running npm start will execute node server.js for you.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

Now install the two packages your chatbot needs.

  • Install the project dependencies by running:
npm install @google/genai express

What are these packages?

  • @google/genai is Google's official SDK for the Gemini API. It handles authentication and sends your messages to the AI model.
  • express is a lightweight web framework for Node.js. It creates the server that connects your frontend to the Gemini API.
  • Confirm the install succeeded by running:
ls

You should see node_modules, package.json, and package-lock.json in the output.

Seeing permission errors or network failures?

If you see EACCES permission errors on macOS, try prefixing the command with sudo. If you see network errors, check your internet connection and try again.

Help me fix npm install errors.

Open package.json again in the editor. You should now see a "dependencies" section that npm added automatically with the two packages listed.

✔️ Awesome, I've got everything!

Great. Double check you have saved package.json and you can see node_modules in your Explorer sidebar.

ⓧ I'd like to double check the full code

Your package.json should look like this:

{
  "name": "gemini-chatbot",
  "version": "1.0.0",
  "description": "A chatbot powered by Google Gemini",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@google/genai": "^2.8.0",
    "express": "^4.21.2"
  }
}

The exact patch versions may differ slightly, but you should see both @google/genai and express listed under dependencies.

Your development environment is ready. Next up, you will get your Gemini API key so your chatbot can talk to Google's AI.

Get Your Gemini API Key

Your development environment is ready with Node.js, VS Code, and all your project dependencies installed.

Now your chatbot needs a way to communicate with Google's Gemini AI. To do that, it needs an API key that proves your identity to Google's servers. In this step, you'll generate a free key and store it securely in your project.

In this step, get ready to:
  • Generate a free Gemini API key from Google AI Studio.
  • Store the key in a .env file.
  • Create a .gitignore file to protect your secrets from version control.
Generate your Gemini API key

Google AI Studio is a free web tool where you can create and manage API keys for the Gemini API. You do not need a Google Cloud billing account for the free tier.

  • Open your web browser and go to aistudio.google.com.
  • Sign in with your Google account.
  • Click Get API Key in the left sidebar.
  • Click Create API Key.
  • A new API key appears on screen. Click the copy icon to copy it to your clipboard.

What is an API key?

An API key is a unique string that identifies your application to Google's servers. Every time your chatbot sends a message to Gemini, it includes this key so Google knows who is making the request.

Your key is free to use. The free tier provides free input and output tokens, which is more than enough for a learning project like this.

Keep your key safe

Treat your API key like a password. Do not share it publicly or commit it to a code repository. You will store it securely in the next substep.

Seeing a permission denied error?

This can happen with Google Workspace (company/school) accounts. Try using a personal Gmail account instead. You can also try opening the page in an incognito browser window.

help me fix a permission denied error when creating a Gemini API key in Google AI Studio

Create your .env file

Your API key needs to live somewhere your code can access it, but where it stays separate from your source code. A .env file is the standard place to store environment-specific secrets.

  • In VS Code, hover over your project folder name in the Explorer sidebar to reveal the action icons.
  • Click the New File icon (the page icon with a plus sign).
  • Type .env as the filename and press Enter.
  • Paste the following into the file, replacing your-api-key-here with the API key you just copied:
GEMINI_API_KEY=your-api-key-here

What does this file do?

The GEMINI_API_KEY environment variable is the name the Google GenAI SDK looks for when authenticating your requests. When you run your server later, you will pass this value to Node.js so your code can read it with process.env.GEMINI_API_KEY.

  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Create your .gitignore file

Your .env file contains a secret. If you ever push this project to GitHub, a .gitignore file tells Git to skip certain files so they never get uploaded.

  • Create another new file in your project folder using the same New File icon in the Explorer sidebar.
  • Name it .gitignore and press Enter.
  • Add the following content to the file:
.env
node_modules/

What do these entries mean?

  • .env prevents your API key from being committed to version control.
  • node_modules/ excludes installed packages. These are large and can always be reinstalled with npm install.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
  • Check the Explorer sidebar. You should see .env and .gitignore listed alongside your package.json and node_modules folder.

Don't see your new files?

Make sure you created the files at the top level of your project folder, not inside node_modules or another subfolder. The files should be siblings of package.json in the Explorer sidebar.

help me find my .env and .gitignore files in VS Code

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is what each file should contain:

GEMINI_API_KEY=your-api-key-here
.env
node_modules/
{
  "name": "gemini-chatbot",
  "version": "1.0.0",
  "description": "A chatbot powered by Google Gemini",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@google/genai": "^2.8.0",
    "express": "^4.21.2"
  }
}

Your API key is generated and safely stored. Next up, you will build the backend server that uses this key to send messages to Gemini.

Build the Backend Server

You have your API key secured and your project dependencies installed. Now it's time to build the brain of your chatbot.

Your backend server handles two jobs: it receives messages from the browser, and it sends those messages to Google's Gemini API for a response. In this step, you will create the server file, wire up the AI, and get your chatbot talking.

In this step, get ready to:
  • Create a server.js file with an Express server that listens on port 3000.
  • Wire up the Google GenAI SDK to handle multi-turn conversations.
  • Add a system instruction that gives your chatbot a custom personality and test it.
Create server.js with an Express server

Your chatbot needs a web server to receive incoming messages and send back responses. Express is a lightweight framework that handles HTTP requests with just a few lines of code.

  • In VS Code, create a new file called server.js in the root of your project folder (the same level as package.json).
  • Add the Express server skeleton by pasting this code:
const express = require("express");
const { GoogleGenAI } = require("@google/genai");

const app = express();
const port = 3000;

// Parse incoming JSON request bodies
app.use(express.json());
// Serve frontend files from the public folder
app.use(express.static("public"));

// Start the server
app.listen(port, () => {
  console.log(`Chatbot server running at http://localhost:${port}`);
});

What does this code do?

This sets up a minimal web server that does two things:

  • express.json() tells Express to automatically parse JSON data from incoming requests. Your frontend will send chat messages as JSON.
  • express.static("public") serves any files inside a public folder directly to the browser. This is where your chat interface will live later.
  • app.listen(port) starts the server on port 3000 and logs a confirmation message.
  • Save server.js.
  • You won't see a visible change yet because the frontend doesn't exist. The next chunk will add the AI logic.

Seeing a red underline or error in VS Code?

Make sure you saved the file with the exact name server.js (not Server.js or server.ts). Also confirm you are saving it at the same level as package.json, not inside a subfolder.

If you see "Cannot find module 'express'", make sure you ran npm install in the previous step.

help me fix a module-not-found error in my server.js file

Wire up the Google GenAI SDK for multi-turn conversations

The server skeleton is ready. Now you need to connect it to Google's Gemini API so it can actually generate responses to user messages.

  • In server.js, find the comment // Start the server. Add this code directly above that comment:
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

const systemInstruction = "You are a friendly and helpful coding mentor named Sparky. You explain concepts clearly with examples, encourage the learner, and keep responses concise (under 3 paragraphs). If asked about something outside of coding and technology, politely redirect the conversation back to tech topics.";

const conversations = new Map();

What does this code do?

These three lines set up the AI layer of your chatbot:

  • new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }) creates an AI client that authenticates with the API key you saved in your .env file.
  • systemInstruction defines your chatbot's personality. It tells Gemini how to behave in every response. You will customize this shortly.
  • conversations is a Map that stores chat history per session. This is what enables multi-turn memory so the bot remembers earlier messages.
  • Now add the chat endpoint. Paste this code directly below the const conversations = new Map(); line and above the // Start the server comment:
app.post("/api/chat", async (req, res) => {
  try {
    const { message, sessionId } = req.body;

    if (!message) {
      return res.status(400).json({ error: "Message is required" });
    }

    const id = sessionId || "default";

    if (!conversations.has(id)) {
      conversations.set(id, []);
    }

    const history = conversations.get(id);

    // Create a chat session with Gemini using the conversation history
    const chat = ai.chats.create({
      model: "gemini-3.5-flash",
      history: history,
      config: {
        systemInstruction: systemInstruction,
      },
    });

    // Send the user's message and get the AI response
    const response = await chat.sendMessage({ message: message });
    const botReply = response.text;

    // Store both turns in history for multi-turn memory
    history.push({ role: "user", parts: [{ text: message }] });
    history.push({ role: "model", parts: [{ text: botReply }] });

    res.json({ reply: botReply });
  } catch (error) {
    console.error("Error:", error.message);
    res.status(500).json({ error: "Something went wrong. Please try again." });
  }
});

What does the /api/chat endpoint do?

This endpoint runs every time the frontend sends a message. Here is the flow:

  • It reads message and sessionId from the request body. The session ID lets multiple users chat without mixing up their histories.
  • It retrieves (or creates) a conversation history array for that session from the conversations Map.
  • ai.chats.create() opens a chat session with the gemini-3.5-flash model, passing the existing history so Gemini knows what was said before.
  • chat.sendMessage() sends the new message and returns the AI's reply.
  • Both the user message and AI response get pushed to history so the next request has full context.
  • Save server.js.

Seeing syntax errors highlighted?

Double-check that the closing }); of the endpoint comes before the app.listen call. The order matters. Your file should flow: imports, app setup, middleware, AI setup, endpoint, then listen.

help me fix a syntax error in my Express endpoint code

Add a system instruction and test your chatbot

The systemInstruction you added tells Gemini exactly how to behave. It is the personality layer that transforms a generic AI into your specific chatbot character named Sparky.

What is a system instruction?

A system instruction is a hidden prompt that the user never sees. It runs before every conversation and shapes all of the AI's responses. Think of it like giving an actor their character brief before they walk on stage.

In your code, the system instruction tells Sparky to be a friendly coding mentor, keep answers short, and stay on topic. You can change this string to make the bot act as a pirate, a Shakespearean actor, or a sarcastic chef.

Time to test everything works. Since your project does not use the dotenv package, you need to pass the API key as an environment variable when you start the server.

  • Start the server by running the command for your operating system:

macOS

  • In the VS Code terminal, run this command:
GEMINI_API_KEY=your-api-key-here node server.js
  • Replace your-api-key-here with the actual API key you saved in your .env file.

Windows

  • In the VS Code terminal, run these commands:
set GEMINI_API_KEY=your-api-key-here && node server.js
  • Replace your-api-key-here with the actual API key you saved in your .env file.
  • You should see this message in your terminal:
Chatbot server running at http://localhost:3000

Your server is running. Now test the chat endpoint directly from your terminal using a second terminal window.

  • Open a second terminal in VS Code by clicking the + icon in the terminal panel.
  • Send a test message to your chatbot by running:

macOS

curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello! What can you help me with?", "sessionId": "test"}'

Windows

curl -X POST http://localhost:3000/api/chat -H "Content-Type: application/json" -d "{\"message\": \"Hello! What can you help me with?\", \"sessionId\": \"test\"}"
  • You should see a JSON response with a reply field containing Sparky's greeting. It will introduce itself as a coding mentor.

What should I see?

The response looks something like this (your exact wording will vary):

{"reply":"Hey there! I'm Sparky, your friendly coding mentor. I can help you with programming concepts, debugging, project ideas, and more. What would you like to learn about today?"}

The exact text will differ each time because AI responses are non-deterministic. As long as you get a JSON object with a reply key, the endpoint is working.

  • Test multi-turn memory by sending a follow-up message with the same sessionId:

macOS

curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "My name is Alex", "sessionId": "test"}'

Windows

curl -X POST http://localhost:3000/api/chat -H "Content-Type: application/json" -d "{\"message\": \"My name is Alex\", \"sessionId\": \"test\"}"
  • Then ask:

macOS

curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "What is my name?", "sessionId": "test"}'

Windows

curl -X POST http://localhost:3000/api/chat -H "Content-Type: application/json" -d "{\"message\": \"What is my name?\", \"sessionId\": \"test\"}"
  • The bot should reply with "Alex" because it remembers the earlier message. That is multi-turn memory in action.

Experiment with different personalities

Try changing the systemInstruction string in server.js to something like "You are a pirate who only speaks in nautical metaphors" or "You are a Shakespearean poet who explains code through sonnets." Restart the server to see how responses change.

  • When you are done testing, press Ctrl+C in the first terminal to stop the server.

Getting an error response instead of a reply?

If you see {"error":"Something went wrong. Please try again."}, check these common causes:

  • Your API key might be incorrect. Double-check the value you passed in the terminal matches the key from Google AI Studio.
  • The environment variable might not be set. Make sure there are no spaces around the = sign in the command.
  • If you see a network error, make sure you have an internet connection. The Gemini API is a cloud service.

help me debug why my Gemini API call is returning an error

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js and that the multi-turn test returned your name correctly.

ⓧ I'd like to double check the full code

Here is the complete server.js file. Make sure yours matches:

const express = require("express");
const { GoogleGenAI } = require("@google/genai");

const app = express();
const port = 3000;

app.use(express.json());
app.use(express.static("public"));

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

const systemInstruction = "You are a friendly and helpful coding mentor named Sparky. You explain concepts clearly with examples, encourage the learner, and keep responses concise (under 3 paragraphs). If asked about something outside of coding and technology, politely redirect the conversation back to tech topics.";

const conversations = new Map();

app.post("/api/chat", async (req, res) => {
  try {
    const { message, sessionId } = req.body;

    if (!message) {
      return res.status(400).json({ error: "Message is required" });
    }

    const id = sessionId || "default";

    if (!conversations.has(id)) {
      conversations.set(id, []);
    }

    const history = conversations.get(id);

    const chat = ai.chats.create({
      model: "gemini-3.5-flash",
      history: history,
      config: {
        systemInstruction: systemInstruction,
      },
    });

    const response = await chat.sendMessage({ message: message });
    const botReply = response.text;

    history.push({ role: "user", parts: [{ text: message }] });
    history.push({ role: "model", parts: [{ text: botReply }] });

    res.json({ reply: botReply });
  } catch (error) {
    console.error("Error:", error.message);
    res.status(500).json({ error: "Something went wrong. Please try again." });
  }
});

app.listen(port, () => {
  console.log(`Chatbot server running at http://localhost:${port}`);
});

Your backend is alive and chatting. Next up, you will build the frontend interface so you can talk to Sparky through a polished chat window instead of terminal commands.

Build the Chat Frontend

Your backend server is ready to receive messages and talk to Gemini. Now you need a way for users to actually see and send those messages.

In this step, you will build the chat interface that connects to your server. The frontend handles three things: displaying messages in a conversation thread, sending user input to your /api/chat endpoint, and showing a loading state while Gemini thinks.

In this step, get ready to:
  • Create the HTML structure with a chat container, message display area, and input form.
  • Style the interface with a dark theme and distinct message bubbles.
  • Write JavaScript to send messages to your API and display responses.
Create the HTML structure

Your server already serves static files from a public folder (thanks to the express.static("public") line in server.js). You need to create that folder and add an HTML file inside it.

  • Create the public folder inside your project by running this command in your terminal:
mkdir public
  • Confirm the folder was created by running:
ls

You should see public listed alongside server.js, package.json, and node_modules.

  • In VS Code's Explorer sidebar, right-click the public folder and select New File.
  • Name the file index.html.
  • Paste the following HTML structure into public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Gemini Chatbot</title>
  <style>
  </style>
</head>
<body>
  <header>
    <h1>Sparky - AI Coding Mentor</h1>
    <p>Powered by Google Gemini</p>
  </header>

  <div id="chat-container"></div>

  <div id="input-area">
    <form id="input-form">
      <input
        type="text"
        id="message-input"
        placeholder="Ask me anything about coding..."
        autocomplete="off"
      />
      <button type="submit" id="send-button">Send</button>
    </form>
  </div>

  <script>
  </script>
</body>
</html>

What does this HTML do?

This sets up three main sections of the chat interface:

  • The <header> displays your chatbot's name (Sparky) and a subtitle.
  • The #chat-container div is where messages will appear as the conversation flows.
  • The #input-form gives the user a text input and a send button to submit messages.
  • Save public/index.html.
  • Open the file directly in your browser by right-clicking index.html in VS Code's Explorer and selecting Open in Default Browser, or by dragging the file into a browser window.
  • You should see unstyled text showing "Sparky - AI Coding Mentor", "Powered by Google Gemini", a text input field, and a Send button on a white background.

Don't see the text?

Make sure you saved the file inside the public folder, not in the project root. The path should be public/index.html.

If the page is completely blank, check for any unclosed HTML tags in your code.

Add the CSS styles

The raw HTML looks plain. You will add a dark theme with styled message bubbles that distinguish user messages from bot responses.

  • In public/index.html, find the empty <style> tag in the <head> section.
  • Add the following CSS rules between the opening <style> and closing </style> tags:
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background-color: #1a1a2e;
      color: #e0e0e0;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }

    header {
      background-color: #16213e;
      padding: 16px 24px;
      text-align: center;
      border-bottom: 1px solid #0f3460;
    }

    header h1 {
      font-size: 1.4rem;
      color: #4fc3f7;
    }

    header p {
      font-size: 0.85rem;
      color: #90a4ae;
      margin-top: 4px;
    }

    #chat-container {
      flex: 1;
      overflow-y: auto;
      padding: 24px;
      display: flex;
      flex-direction: column;
      gap: 16px;
    }

    .message {
      max-width: 75%;
      padding: 12px 16px;
      border-radius: 16px;
      line-height: 1.5;
      font-size: 0.95rem;
      white-space: pre-wrap;
    }

    .message.user {
      align-self: flex-end;
      background-color: #0f3460;
      color: #e0e0e0;
      border-bottom-right-radius: 4px;
    }

    .message.bot {
      align-self: flex-start;
      background-color: #1e3a5f;
      color: #e0e0e0;
      border-bottom-left-radius: 4px;
    }

    .message.bot.loading {
      color: #90a4ae;
      font-style: italic;
    }

    #input-area {
      background-color: #16213e;
      padding: 16px 24px;
      border-top: 1px solid #0f3460;
    }

    #input-form {
      display: flex;
      gap: 12px;
      max-width: 800px;
      margin: 0 auto;
    }

    #message-input {
      flex: 1;
      padding: 12px 16px;
      border: 1px solid #0f3460;
      border-radius: 24px;
      background-color: #1a1a2e;
      color: #e0e0e0;
      font-size: 0.95rem;
      outline: none;
    }

    #message-input:focus {
      border-color: #4fc3f7;
    }

    #send-button {
      padding: 12px 24px;
      background-color: #4fc3f7;
      color: #1a1a2e;
      border: none;
      border-radius: 24px;
      font-size: 0.95rem;
      font-weight: 600;
      cursor: pointer;
    }

    #send-button:hover {
      background-color: #81d4fa;
    }

    #send-button:disabled {
      background-color: #37474f;
      color: #78909c;
      cursor: not-allowed;
    }

What do these styles do?

The CSS creates a dark-themed chat interface with three distinct zones:

  • The body uses flexbox to stack the header, chat area, and input from top to bottom, filling the full viewport height.
  • User messages (.message.user) align to the right in a darker blue. Bot messages (.message.bot) align to the left in a slightly lighter blue.
  • The send button changes appearance when hovered and when disabled, giving the user visual feedback.
  • Save public/index.html and refresh the browser tab where you opened the file.
  • You should see a dark background with a blue header reading "Sparky - AI Coding Mentor" and a rounded input field with a blue Send button at the bottom.

Still seeing a white background?

Make sure your CSS is placed between the <style> and </style> tags inside the <head> section. If it is pasted outside those tags, the browser will ignore it.

Also check that you did not accidentally delete the </style> closing tag.

Write the chat JavaScript

The interface looks good, but it cannot do anything yet. You need JavaScript to capture the user's message, send it to your server's /api/chat endpoint, and display the response in the chat window.

  • In public/index.html, find the empty <script> tag near the bottom of the file (just before </body>).
  • Add the following JavaScript between the opening <script> and closing </script> tags:
    const chatContainer = document.getElementById("chat-container");
    const inputForm = document.getElementById("input-form");
    const messageInput = document.getElementById("message-input");
    const sendButton = document.getElementById("send-button");

    const sessionId = crypto.randomUUID();

    function addMessage(text, sender) {
      const messageDiv = document.createElement("div");
      messageDiv.classList.add("message", sender);
      messageDiv.textContent = text;
      chatContainer.appendChild(messageDiv);
      chatContainer.scrollTop = chatContainer.scrollHeight;
      return messageDiv;
    }

    async function sendMessage(userMessage) {
      addMessage(userMessage, "user");
      messageInput.value = "";
      sendButton.disabled = true;

      const loadingDiv = addMessage("Thinking...", "bot");
      loadingDiv.classList.add("loading");

      try {
        const response = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ message: userMessage, sessionId: sessionId }),
        });

        const data = await response.json();

        chatContainer.removeChild(loadingDiv);

        if (data.error) {
          addMessage("Sorry, something went wrong. Please try again.", "bot");
        } else {
          addMessage(data.reply, "bot");
        }
      } catch (error) {
        chatContainer.removeChild(loadingDiv);
        addMessage("Connection error. Is the server running?", "bot");
      }

      sendButton.disabled = false;
      messageInput.focus();
    }

    inputForm.addEventListener("submit", (e) => {
      e.preventDefault();
      const message = messageInput.value.trim();
      if (message) {
        sendMessage(message);
      }
    });

    messageInput.focus();

What does this JavaScript do?

The script has four main parts:

  • DOM references grab the chat container, form, input, and button so the script can manipulate them.
  • The addMessage() function creates a new message bubble and appends it to the chat, auto-scrolling to the bottom.
  • The sendMessage() function posts the user's text to /api/chat, shows a "Thinking..." indicator while waiting, then displays the bot's reply.
  • The sessionId generated by crypto.randomUUID() is sent with every request so the server tracks this browser tab's conversation history.

What is a REST API?

A REST API is a way for two programs to communicate over the web using standard HTTP methods like GET and POST. Your frontend sends a POST request to /api/chat with the user's message, and the server responds with JSON containing the bot's reply.

This client-server pattern is how most modern web apps work. The frontend handles the UI, and the backend handles the logic and external API calls.

  • Save public/index.html.

You will not see the chat working yet because the JavaScript needs the server running to send messages. In the next step, you will start the server and test everything together.

✔️ Awesome, I've got everything!

Great. Make sure you have saved public/index.html with all three sections (HTML structure, CSS styles, and JavaScript) in place.

ⓧ I'd like to double check the full code

Here is the complete public/index.html file. Compare your version against this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Gemini Chatbot</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background-color: #1a1a2e;
      color: #e0e0e0;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }

    header {
      background-color: #16213e;
      padding: 16px 24px;
      text-align: center;
      border-bottom: 1px solid #0f3460;
    }

    header h1 {
      font-size: 1.4rem;
      color: #4fc3f7;
    }

    header p {
      font-size: 0.85rem;
      color: #90a4ae;
      margin-top: 4px;
    }

    #chat-container {
      flex: 1;
      overflow-y: auto;
      padding: 24px;
      display: flex;
      flex-direction: column;
      gap: 16px;
    }

    .message {
      max-width: 75%;
      padding: 12px 16px;
      border-radius: 16px;
      line-height: 1.5;
      font-size: 0.95rem;
      white-space: pre-wrap;
    }

    .message.user {
      align-self: flex-end;
      background-color: #0f3460;
      color: #e0e0e0;
      border-bottom-right-radius: 4px;
    }

    .message.bot {
      align-self: flex-start;
      background-color: #1e3a5f;
      color: #e0e0e0;
      border-bottom-left-radius: 4px;
    }

    .message.bot.loading {
      color: #90a4ae;
      font-style: italic;
    }

    #input-area {
      background-color: #16213e;
      padding: 16px 24px;
      border-top: 1px solid #0f3460;
    }

    #input-form {
      display: flex;
      gap: 12px;
      max-width: 800px;
      margin: 0 auto;
    }

    #message-input {
      flex: 1;
      padding: 12px 16px;
      border: 1px solid #0f3460;
      border-radius: 24px;
      background-color: #1a1a2e;
      color: #e0e0e0;
      font-size: 0.95rem;
      outline: none;
    }

    #message-input:focus {
      border-color: #4fc3f7;
    }

    #send-button {
      padding: 12px 24px;
      background-color: #4fc3f7;
      color: #1a1a2e;
      border: none;
      border-radius: 24px;
      font-size: 0.95rem;
      font-weight: 600;
      cursor: pointer;
    }

    #send-button:hover {
      background-color: #81d4fa;
    }

    #send-button:disabled {
      background-color: #37474f;
      color: #78909c;
      cursor: not-allowed;
    }
  </style>
</head>
<body>
  <header>
    <h1>Sparky - AI Coding Mentor</h1>
    <p>Powered by Google Gemini</p>
  </header>

  <div id="chat-container"></div>

  <div id="input-area">
    <form id="input-form">
      <input
        type="text"
        id="message-input"
        placeholder="Ask me anything about coding..."
        autocomplete="off"
      />
      <button type="submit" id="send-button">Send</button>
    </form>
  </div>

  <script>
    const chatContainer = document.getElementById("chat-container");
    const inputForm = document.getElementById("input-form");
    const messageInput = document.getElementById("message-input");
    const sendButton = document.getElementById("send-button");

    const sessionId = crypto.randomUUID();

    function addMessage(text, sender) {
      const messageDiv = document.createElement("div");
      messageDiv.classList.add("message", sender);
      messageDiv.textContent = text;
      chatContainer.appendChild(messageDiv);
      chatContainer.scrollTop = chatContainer.scrollHeight;
      return messageDiv;
    }

    async function sendMessage(userMessage) {
      addMessage(userMessage, "user");
      messageInput.value = "";
      sendButton.disabled = true;

      const loadingDiv = addMessage("Thinking...", "bot");
      loadingDiv.classList.add("loading");

      try {
        const response = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ message: userMessage, sessionId: sessionId }),
        });

        const data = await response.json();

        chatContainer.removeChild(loadingDiv);

        if (data.error) {
          addMessage("Sorry, something went wrong. Please try again.", "bot");
        } else {
          addMessage(data.reply, "bot");
        }
      } catch (error) {
        chatContainer.removeChild(loadingDiv);
        addMessage("Connection error. Is the server running?", "bot");
      }

      sendButton.disabled = false;
      messageInput.focus();
    }

    inputForm.addEventListener("submit", (e) => {
      e.preventDefault();
      const message = messageInput.value.trim();
      if (message) {
        sendMessage(message);
      }
    });

    messageInput.focus();
  </script>
</body>
</html>

Your frontend is complete. Next up, you will start the server and have your first conversation with Sparky.

Test Your Chatbot

In the previous step, you built a polished chat interface that sends messages to your backend and displays responses.

Now it's time to bring everything together by starting the server and having a real conversation with your chatbot. You'll verify that Sparky responds intelligently and remembers what you've said across multiple messages.

In this step, get ready to:
  • Start your server and load the chat interface in your browser.
  • Verify multi-turn conversation memory works.
  • Experiment with different chatbot personalities.
Start your server

Your server code reads the GEMINI_API_KEY from your system's environment variables. Since the project doesn't use a library to auto-load the .env file, you need to pass the key directly when starting the server.

  • Return to the terminal in VS Code.
  • Start the server by running the command for your operating system below. Replace your-api-key-here with the actual API key you saved in your .env file earlier.

macOS

GEMINI_API_KEY=your-api-key-here node server.js

Windows

set GEMINI_API_KEY=your-api-key-here && node server.js

What does this command do?

On macOS, prefixing a command with VARIABLE=value sets that variable just for the duration of that command. On Windows, set creates the variable for the current terminal session.

Your server reads this variable via process.env.GEMINI_API_KEY and passes it to the GoogleGenAI constructor to authenticate with Google's API.

  • You should see this message in your terminal:
Chatbot server running at http://localhost:3000
  • Open your browser and navigate to http://localhost:3000.

You should see the Sparky chat interface with a dark blue background, a header that says Sparky - AI Coding Mentor, and an input field at the bottom.

Seeing an error in your terminal?

  • If you see a message about GEMINI_API_KEY being undefined or an authentication error, double-check that you replaced your-api-key-here with your actual API key from Google AI Studio.
  • If you see a port error, another process may already be using port 3000. Close other terminal sessions and try again.
  • On Windows, make sure there is no space between your key and && in the command.

Help me troubleshoot my server startup error.

Test multi-turn conversation

The real test of your chatbot is whether it can remember context across multiple messages. This is the multi-turn conversation feature you built using the chats helper and conversation history.

  • Type My name is Alex into the input field and click Send.
  • Wait for Sparky to respond. You should see a greeting that acknowledges your name.
  • Now type What's my name? and click Send.

Sparky should reply with your name, proving it remembers the earlier message. This works because your server stores the conversation history in the conversations Map and passes it back to Gemini with each request.

  • Try a few more coding-related questions to see how Sparky behaves. For example, type What is a REST API? or Can you explain variables in JavaScript?.

Why does Sparky only talk about coding?

Your systemInstruction tells Gemini to politely redirect non-tech questions back to coding topics. Try asking about something unrelated (like a recipe) to see this in action.

Not getting a response?

  • Check your terminal for error messages. A 400 error usually means your API key is invalid or expired.
  • Make sure you are using the API key from Google AI Studio (aistudio.google.com), not a Google Cloud key.
  • If you see a rate limit error, wait a minute and try again. The free tier has usage limits.

Help me debug why my chatbot isn't responding to messages.

Experiment with the system instruction

The systemInstruction in your server.js file is what gives Sparky its personality. Changing this string completely transforms how the chatbot behaves.

  • Stop your server by pressing Ctrl+C in the terminal.
  • In server.js, find the systemInstruction line:
const systemInstruction = "You are a friendly and helpful coding mentor named Sparky. You explain concepts clearly with examples, encourage the learner, and keep responses concise (under 3 paragraphs). If asked about something outside of coding and technology, politely redirect the conversation back to tech topics.";
  • Replace the text inside the quotes with a different personality. For example, try making Sparky a pirate:
const systemInstruction = "You are a pirate coding mentor named Captain Sparky. You explain coding concepts using nautical metaphors and pirate slang. Keep responses fun and under 3 paragraphs.";
  • Save server.js and restart your server with the same command you used earlier (including your API key).
  • Refresh your browser and send a message like What is a function? to see the new personality in action.

You should notice a completely different tone in Sparky's responses. The system instruction acts as a hidden directive that shapes every reply.

Try a few personalities

Experiment with different system instructions. Try a Shakespearean tutor, a sarcastic robot, or a patient kindergarten teacher. Each change shows how powerful prompt engineering is for controlling AI behavior.

  • When you're done experimenting, change the systemInstruction back to the original text:
const systemInstruction = "You are a friendly and helpful coding mentor named Sparky. You explain concepts clearly with examples, encourage the learner, and keep responses concise (under 3 paragraphs). If asked about something outside of coding and technology, politely redirect the conversation back to tech topics.";
  • Save server.js and restart the server one more time.

Why restart the server each time?

Unlike frontend files that refresh in the browser, changes to server.js require a full server restart. Node.js loads the file once at startup. Any edits only take effect after stopping (Ctrl+C) and restarting the process.

Secret mission

Add Streaming Responses

Your chatbot works, but the responses pop in all at once. Real AI chat apps like ChatGPT show text appearing word-by-word. Can you add streaming responses to give your chatbot that premium, real-time feel?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, and the Gemini API free tier has no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder (gemini-chatbot) with all source files.
  • node_modules/ folder containing installed packages.
  • Gemini API key created in Google AI Studio.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your chatbot.

  • Your project folder stays on your Desktop ready to use.
  • Your Gemini API key remains active in Google AI Studio for future projects.
  • Node.js and VS Code remain installed on your machine.

Pause - I'll come back to this later

Stop the running server to free up your terminal and system resources. Your files and API key stay intact for next time.

  • In your terminal, press Ctrl+C to stop the Node.js server.
  • Close the browser tab running your chatbot.
  • When you return, start the server again with the same command you used before.

Delete - I don't want to use this again

Remove all project files and revoke your API key so nothing is left behind.

  • In your terminal, press Ctrl+C to stop the server if it is still running.
  • Delete the project folder by running this command (macOS/Linux):
cd ~/Desktop
rm -rf gemini-chatbot

On Windows, run this instead:

cd ~\Desktop
rmdir /s /q gemini-chatbot
  • Delete your Gemini API key by visiting aistudio.google.com/apikey. Find your key in the list and delete it from the API Keys page.

Nice Work!

Nice Work!

You just built a fully functional AI chatbot from scratch. Your app connects a polished chat interface to Google's Gemini AI, handles multi-turn conversations, and runs entirely on your local machine.

You've learned how to:

  • Built a Node.js backend that authenticates with the Google Gemini API and manages multi-turn conversation history.
  • Created a client-server architecture with a polished chat UI built in HTML, CSS, and JavaScript that communicates with your Express API.
  • Configured a system instruction to give your chatbot a custom personality and observed how it shapes every response.
  • Added streaming responses using Server-Sent Events so messages appear word-by-word for a real-time chat experience.

Ready to quiz yourself?