AI Customer Support with Next.js
Build a streaming AI support chatbot with Next.js, Gemini, and the Vercel AI SDK.
Introduction
30 Second Summary
Every time you chat with a support bot on a website, there is a full-stack application behind it. It streams responses in real time, manages conversation history, and connects to an AI model through a typed API layer.
In this project, you will build an AI-powered customer support assistant from scratch using Next.js and Google Gemini. You will create a streaming API route, wire it to a polished chat interface, and learn the exact TypeScript patterns that AI product teams ship every day.
What You'll Build
A full-stack AI customer support assistant for a fictional SaaS product, with a streaming API route, a real-time chat interface, and a structured system prompt that constrains the AI to act as a knowledgeable support agent.
By the end of this project, you'll have:
- A streaming API route that integrates Gemini via the Vercel AI SDK provider pattern.
- A React chat interface with real-time message streaming, loading states, and error handling.
- A type-safe architecture with shared types across client and server boundaries.
- Secret Mission: Add a tool that lets Gemini look up order status from a mock database, demonstrating AI tool calling.
Are there any prerequisites?
You should be familiar with TypeScript and React basics. You will also need a free Google AI Studio API key (no billing required).
Set Up the Project
Every AI product starts with a solid foundation. Before you can build a streaming chatbot, you need a Next.js project configured with TypeScript and the right AI packages.
In this step, you will install Node.js, scaffold a Next.js app, and connect it to Google Gemini through the Vercel AI SDK. By the end, your dev server will be running and ready for feature work.
In this step, get ready to:
- Install Node.js and create the Next.js project.
- Install the AI SDK packages and configure your API key.
- Verify the dev server runs at localhost.
Install Node.js and create the Next.js project
Next.js requires Node.js version 20.9 or higher. Let's check whether you have it installed.
- 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 your Node.js version by running this command:
node --version
✔️ I see version 20.9 or higher
Your Node.js version is compatible. You are ready to create the project.
ⓧ I see an older version
You have Node.js installed, but the version is below 20.9. You need to upgrade.
- Visit https://nodejs.org and download the latest LTS installer for your operating system.
- Run the installer and follow the prompts to complete the upgrade.
- Close and reopen your terminal, then run node --version again to confirm you see 20.9 or higher.
ⓧ Command not found
Node.js is not installed on your machine. Let's install it now.
- Visit https://nodejs.org and download the latest LTS installer for your operating system.
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run node --version to confirm it shows 20.9 or higher.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Next.js project by running this command:
npx create-next-app@latest support-assistant --typescript --tailwind --app --use-npm
What does this command do?
This scaffolds a new Next.js project in a folder called support-assistant with TypeScript, Tailwind CSS, and the App Router pre-configured.
Because you passed explicit flags, the CLI skips all interactive prompts and uses recommended defaults for everything else (ESLint enabled, import alias @/*). You will see a summary of the resolved defaults printed to your terminal.
- Wait for the installation to finish. You should see a success message confirming the project was created.
- Move into the new project folder by running:
cd support-assistant
Seeing a permission error or network timeout?
Make sure you have a stable internet connection. The command downloads the project template from npm.
On macOS, if you see a permission error, try prefixing the command with sudo. On Windows, try running your terminal as Administrator.
help me troubleshoot my create-next-app installation error
Install the AI SDK packages and configure your API key
The Vercel AI SDK provides a unified interface for calling AI models and streaming their responses. The @ai-sdk/google package connects that interface to Google Gemini.
- Install the AI SDK packages by running:
npm install ai @ai-sdk/google
What are these packages?
The ai package provides core functions like streamText and React hooks like useChat. The @ai-sdk/google package is the provider that knows how to talk to Google's Gemini models.
This provider pattern means you can swap to a different AI model later by changing a single import. All other code stays the same.
- Confirm the install succeeded. You should see output listing the added packages with no errors.
npm install failed?
Make sure you are inside the support-assistant folder (run pwd to check). If you see a network error, check your internet connection and try again.
help me fix my npm install error for the AI SDK packages
Now you need a free API key from Google AI Studio so your app can call Gemini.
- Open https://aistudio.google.com/apikey in your browser.
- Sign in with your Google account.
- Click Create API Key and copy the generated key.
Is this free?
Yes. The Gemini API free tier includes input and output tokens at no charge. No billing setup or credit card is required.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the support-assistant folder, and click Open.
- In the Explorer sidebar, right-click and select New File.
- Name the file .env.local and press Enter.
- Paste the following into .env.local, replacing your-api-key-here with the key you just copied:
GOOGLE_GENERATIVE_AI_API_KEY=your-api-key-here
Why .env.local?
Next.js automatically loads environment variables from .env.local and keeps them out of version control. The @ai-sdk/google provider reads GOOGLE_GENERATIVE_AI_API_KEY automatically, so you never need to pass the key in code.
- Save .env.local.
✔️ Awesome, I've got everything!
Great. Make sure you have saved .env.local with your real API key in place of the placeholder.
ⓧ I'd like to double check the full code
Here is the complete .env.local file:
GOOGLE_GENERATIVE_AI_API_KEY=your-api-key-here
Verify the dev server starts
Let's confirm everything is wired up correctly by starting the development server.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Start the dev server by running:
npm run dev
- Open your browser and navigate to http://localhost:3000.
- You should see the default Next.js welcome page with the Next.js logo and some starter text.
Page not loading?
Check your terminal for error messages. If you see a port conflict, another process is using port 3000. Stop it or run npm run dev -- --port 3001 instead.
If you see a module-not-found error, make sure you ran npm install ai @ai-sdk/google from inside the support-assistant folder.
help me get my Next.js dev server running
Your development environment is ready. Next up, you will build a streaming API route that connects to Gemini and returns AI responses in real time.
Build the Streaming API Route
Your development environment is ready and the AI SDK packages are installed. Now it's time to build the server-side route that powers your chatbot.
Next.js Route Handlers let you define API endpoints directly inside your app folder. You'll create one that accepts chat messages, sends them to Gemini, and streams the response back in real time.
In this step, get ready to:
- Define the assistant's persona and product knowledge in a system prompt.
- Build a streaming POST route using the Vercel AI SDK.
- Test the endpoint to confirm Gemini streams back a response.
Create the system prompt and route handler
Before the route can respond to chat messages, it needs to know what role to play. You'll define the assistant's persona and product knowledge in a dedicated file, then wire it into the streaming route.
- In VS Code, create a new folder called lib inside your support-assistant project folder.
- Inside lib, create a new file called types.ts.
- Add the following product tier definitions by pasting this code:
export interface ProductTier {
name: string;
price: string;
features: string[];
}
export interface OrderInfo {
orderId: string;
status: string;
product: string;
date: string;
}
export const PRODUCT_TIERS: ProductTier[] = [
{
name: "Starter",
price: "$9/month",
features: ["5 projects", "1GB storage", "Email support"],
},
{
name: "Pro",
price: "$29/month",
features: ["Unlimited projects", "10GB storage", "Priority support", "API access"],
},
{
name: "Enterprise",
price: "Custom pricing",
features: ["Everything in Pro", "Dedicated account manager", "SSO", "99.9% SLA"],
},
];
What does this code do?
This file defines the TypeScript interfaces and product data that the system prompt will reference. The ProductTier interface shapes each pricing plan, and PRODUCT_TIERS holds the actual plan details the AI will use when answering pricing questions.
The OrderInfo interface will be used later in the Secret Mission for order lookups.
- Save lib/types.ts.
- You won't see a change in the browser yet. The next file uses these types to build the system prompt.
- Inside the lib folder, create another new file called prompts.ts.
- Add the system prompt that defines the assistant's persona by pasting this code:
import { PRODUCT_TIERS } from "./types";
export const SYSTEM_PROMPT = `You are a helpful customer support assistant for Acme SaaS, a project management platform.
You help customers with:
- Pricing and plan questions
- Feature explanations
- Troubleshooting common issues
- Account and billing inquiries
Our pricing tiers:
${PRODUCT_TIERS.map(
(tier) => `- ${tier.name} (${tier.price}): ${tier.features.join(", ")}`
).join("\n")}
Common troubleshooting:
- If a user cannot log in, suggest resetting their password at acme.example.com/reset
- If a project is not loading, suggest clearing browser cache and trying again
- If API requests are failing, check that the API key is valid and has not expired
Guidelines:
- Be concise and helpful
- If you do not know the answer, say so honestly and offer to connect them with a human agent
- Never make up information about features or pricing
- Use a friendly, professional tone`;
What does this code do?
The SYSTEM_PROMPT constant is a template literal that dynamically inserts the product tier data. This gives Gemini specific knowledge about Acme SaaS's pricing, features, and common issues.
System prompts are instructions that the AI follows on every message. They shape the assistant's personality and constrain what it says, without the user ever seeing them.
- Save lib/prompts.ts.
- You won't see a change in the browser yet. The route handler you create next will import this prompt.
- Inside the app folder, create a nested folder structure: api/chat.
- Inside app/api/chat, create a new file called route.ts.
- Build the streaming route handler by pasting this code:
import { streamText } from "ai";
import { google } from "@ai-sdk/google";
import { SYSTEM_PROMPT } from "@/lib/prompts";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
// Extract the chat messages from the request body
const { messages } = await req.json();
// Call Gemini with the system prompt and conversation history
const result = streamText({
model: google("gemini-2.5-flash"),
system: SYSTEM_PROMPT,
messages,
});
// Return the streaming response to the client
return result.toDataStreamResponse();
}
What does this code do?
This is a Next.js Route Handler. Exporting a POST function from app/api/chat/route.ts automatically creates a POST endpoint at /api/chat.
The streamText function from the Vercel AI SDK sends the messages to Gemini and receives tokens as they are generated. The toDataStreamResponse() method converts that stream into a format the frontend can consume in real time.
The google("gemini-2.5-flash") call creates a model instance using the @ai-sdk/google provider. It reads your GOOGLE_GENERATIVE_AI_API_KEY environment variable automatically.
- Save app/api/chat/route.ts.
- Check the terminal where your dev server is running. It should recompile without showing any import or type errors.
Seeing import errors in the terminal?
Make sure the lib folder is at the top level of your support-assistant project (next to the app folder), not inside app.
Confirm that lib/types.ts and lib/prompts.ts both exist and are saved. The @/lib/prompts path relies on the @ alias that Next.js configured for you.
help me fix import errors in my Next.js route handler
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
ⓧ I'd like to double check the full code
import { streamText } from "ai";
import { google } from "@ai-sdk/google";
import { SYSTEM_PROMPT } from "@/lib/prompts";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: google("gemini-2.5-flash"),
system: SYSTEM_PROMPT,
messages,
});
return result.toDataStreamResponse();
}
Understand the streaming response pattern
Two pieces of the route handler deserve a closer look: maxDuration and toDataStreamResponse().
Why maxDuration and toDataStreamResponse?
By default, serverless functions have a short execution timeout. Setting export const maxDuration = 30 tells the runtime to allow this route to stay open for up to 30 seconds. AI responses can take several seconds to finish streaming, so this prevents premature cutoffs.
The toDataStreamResponse() method returns an HTTP response that uses Server-Sent Events to push tokens to the client as they arrive. Instead of waiting for the entire response to generate, the user sees words appear progressively. This is the same streaming pattern ChatGPT and other AI products use.
What is the AI SDK provider pattern?
The google("gemini-2.5-flash") call uses the AI SDK's provider pattern. Every AI provider (Google, OpenAI, Anthropic) exposes the same interface. If you ever want to switch models, you just swap the provider import and the model string. The rest of your code stays identical.
Test the API route
Your route is ready. You'll send a test request to confirm that Gemini responds with a streamed message.
- Open a second terminal window (keep the dev server running in the first one).
- Send a test request to the route by running this command:
macOS
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "What plans do you offer?"}]}'
Windows
curl.exe -X POST http://localhost:3000/api/chat -H "Content-Type: application/json" -d '{"messages": [{"role": "user", "content": "What plans do you offer?"}]}'
What should I see?
You should see a stream of data chunks in your terminal containing the assistant's response about Acme SaaS pricing plans. The output will include data lines with text fragments that form a complete answer about Starter, Pro, and Enterprise tiers.
The response arrives incrementally rather than all at once. This confirms streaming is working correctly.
Getting an empty response or an error?
- If you see a 500 error, check that your .env.local file contains a valid GOOGLE_GENERATIVE_AI_API_KEY value. Get a free key at https://aistudio.google.com/apikey.
- If you see a connection refused error, confirm the dev server is still running in your first terminal on port 3000.
- On Windows, make sure you use curl.exe (not just curl). PowerShell aliases curl to a different command that uses incompatible syntax.
help me debug my streaming API route that returns an error
Your backend is live and streaming. Next up, you'll build a chat interface that connects to this route and displays Gemini's responses in real time.
Build the Chat Interface
Your API route is streaming Gemini responses. Now you need a frontend that actually displays them.
The Vercel AI SDK provides a React hook called useChat that handles the entire client-side chat lifecycle. It manages message history, input state, streaming, errors, and even aborting mid-response.
In this step, get ready to:
- Replace the default page with a chat component powered by the useChat hook.
- Build a styled message list with distinct user and assistant bubbles.
- Add error handling and a stop-generation button.
Replace the page with the chat component
The useChat hook connects to your /api/chat route automatically. It returns everything you need to build a chat UI: the message array, the input value, change and submit handlers, a loading flag, error state, and a stop function.
- Open app/page.tsx in your editor.
- Select all the existing content and replace it with this code:
"use client";
import { useChat } from "@ai-sdk/react";
// Suggested questions displayed when the chat is empty
const SUGGESTED_QUESTIONS = [
"What pricing plans do you offer?",
"How do I reset my password?",
"What features are included in the Pro plan?",
];
export default function Home() {
// useChat connects to /api/chat and manages the full chat lifecycle
const { messages, input, handleInputChange, handleSubmit, isLoading, error, stop } =
useChat();
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<header className="text-center py-4 border-b">
<h1 className="text-2xl font-bold">Acme Support</h1>
<p className="text-gray-500 text-sm">AI-powered customer support</p>
</header>
<div className="flex-1 overflow-y-auto py-4 space-y-4">
{/* Welcome message and suggested questions when no messages exist */}
{messages.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">
Hi! I am the Acme SaaS support assistant. How can I help you today?
</p>
<div className="flex flex-wrap gap-2 justify-center">
{SUGGESTED_QUESTIONS.map((question) => (
<button
key={question}
onClick={() => {
handleInputChange({
target: { value: question },
} as React.ChangeEvent<HTMLInputElement>);
}}
className="px-3 py-1 bg-blue-50 text-blue-700 rounded-full text-sm hover:bg-blue-100 transition"
>
{question}
</button>
))}
</div>
</div>
)}
{/* Message list with distinct styling for user and assistant */}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))}
{/* Loading indicator while assistant is responding */}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-lg px-4 py-2 text-gray-500">
Typing...
</div>
</div>
)}
</div>
{/* Error display */}
{error && (
<div className="bg-red-50 text-red-700 px-4 py-2 rounded-lg mb-2">
Something went wrong. Please try again.
</div>
)}
{/* Input form with conditional Stop/Send button */}
<form onSubmit={handleSubmit} className="flex gap-2 border-t pt-4">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your question..."
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition"
>
Stop
</button>
) : (
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Send
</button>
)}
</form>
</div>
);
}
What does this code do?
The useChat() hook automatically POSTs to /api/chat whenever the form is submitted. It streams the response and appends each chunk to the messages array in real time.
- messages holds the full chat history. Each message has a role ("user" or "assistant") and content.
- handleInputChange and handleSubmit wire up the input and form so the hook controls the chat flow.
- isLoading is true while the assistant is streaming. The UI uses it to show "Typing..." and swap the Send button for a Stop button.
- error surfaces any fetch failure as a red banner above the input.
- stop() aborts the streaming response mid-sentence, saving tokens and giving the user control.
- Save app/page.tsx.
- Return to your browser at http://localhost:3000. You should see the "Acme Support" header, a welcome message, and three blue suggested-question buttons.
Don't see the chat UI?
- Check your terminal for compilation errors. A missing closing brace or unclosed JSX tag will prevent the page from rendering.
- Make sure the file starts with "use client"; on the very first line. Without it, Next.js treats the component as a Server Component and useChat will throw an error.
- Confirm your dev server is still running. If it stopped, run npm run dev again from your project folder.
Why isn't my chat UI rendering?
✔️ Awesome, I've got everything!
Great. Double check you have saved app/page.tsx and can see the welcome screen in your browser.
ⓧ I'd like to double check the full code
"use client";
import { useChat } from "@ai-sdk/react";
const SUGGESTED_QUESTIONS = [
"What pricing plans do you offer?",
"How do I reset my password?",
"What features are included in the Pro plan?",
];
export default function Home() {
const { messages, input, handleInputChange, handleSubmit, isLoading, error, stop } =
useChat();
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<header className="text-center py-4 border-b">
<h1 className="text-2xl font-bold">Acme Support</h1>
<p className="text-gray-500 text-sm">AI-powered customer support</p>
</header>
<div className="flex-1 overflow-y-auto py-4 space-y-4">
{messages.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">
Hi! I am the Acme SaaS support assistant. How can I help you today?
</p>
<div className="flex flex-wrap gap-2 justify-center">
{SUGGESTED_QUESTIONS.map((question) => (
<button
key={question}
onClick={() => {
handleInputChange({
target: { value: question },
} as React.ChangeEvent<HTMLInputElement>);
}}
className="px-3 py-1 bg-blue-50 text-blue-700 rounded-full text-sm hover:bg-blue-100 transition"
>
{question}
</button>
))}
</div>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-lg px-4 py-2 text-gray-500">
Typing...
</div>
</div>
)}
</div>
{error && (
<div className="bg-red-50 text-red-700 px-4 py-2 rounded-lg mb-2">
Something went wrong. Please try again.
</div>
)}
<form onSubmit={handleSubmit} className="flex gap-2 border-t pt-4">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your question..."
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition"
>
Stop
</button>
) : (
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Send
</button>
)}
</form>
</div>
);
}
Test the message rendering and streaming
The chat UI is ready to talk to your API route. Time to send your first message and watch streaming in action.
- Click one of the suggested question buttons (for example, "What pricing plans do you offer?"). The text fills the input field.
- Click Send or press Enter.
- Watch the response stream in. Your message appears on the right in a blue bubble. The assistant's response appears on the left in a gray bubble, with text arriving word by word.
How does the message styling work?
Each message's role property determines its position and color. User messages get justify-end (right-aligned, blue background). Assistant messages get justify-start (left-aligned, gray background).
The isLoading flag shows the "Typing..." indicator as soon as your message is sent. It disappears once the full response arrives.
Message sent but nothing streaming back?
- Open your browser's DevTools (F12) and check the Network tab. Look for a POST request to /api/chat. If you see a 500 error, your API key in .env.local may be invalid.
- If you see no network request at all, confirm the form has onSubmit={handleSubmit} and the button type is submit.
Help me debug why my chat messages are not streaming back from the API.
Test error handling and stop generation
Production chat interfaces need graceful failure and user control. Your UI already handles both.
- Send another question (type anything in the input and press Enter).
- While the response is streaming in, click the red Stop button. The response cuts off immediately and the button switches back to Send.
How does the Stop button work?
The stop() function aborts the underlying fetch request. The AI SDK stops reading chunks from the server and the isLoading flag flips to false. The partial response stays in the message list.
The error state only appears when the fetch itself fails (network issue, 500 response, invalid API key). If you want to test it, temporarily rename your environment variable in .env.local and send a message. You will see the red error banner appear above the input.
Your chat interface is fully functional. Messages stream in real time, errors display gracefully, and users can stop generation mid-response. Next up, you will give the assistant structured knowledge by refining the system prompt and adding shared types.
Refine the System Prompt and Add Types
Your chat UI streams messages and handles errors, but the route handler currently imports from a prompt file that doesn't exist yet. Right now, your dev server is showing a module-not-found error because @/lib/prompts hasn't been created.
In this step, you'll create the TypeScript files that give your assistant its personality and domain knowledge. A well-crafted system prompt is what separates a generic chatbot from a useful support agent.
In this step, get ready to:
- Define TypeScript interfaces and product data for the support assistant.
- Create a structured system prompt with Acme SaaS domain knowledge.
- Verify the assistant responds with specific pricing and troubleshooting information.
Create type definitions for your product data
Before building the system prompt, you need a data structure that describes Acme SaaS's pricing tiers. Defining this as typed data (rather than hardcoding strings in the prompt) makes the information reusable and easy to update.
- In VS Code, create a new folder called lib at the root of your support-assistant project (at the same level as app).
- Create a new file inside lib called types.ts.
- Add the following interfaces that define the shape of your product and order data:
export interface ProductTier {
name: string;
price: string;
features: string[];
}
export interface OrderInfo {
orderId: string;
status: string;
product: string;
date: string;
}
What do these interfaces do?
- ProductTier defines the shape of each pricing plan: a name, price string, and list of features.
- OrderInfo defines order data you'll use later in the Secret Mission for order lookups.
- Below the interfaces in the same file, add the product tier data that your system prompt will reference:
export const PRODUCT_TIERS: ProductTier[] = [
{
name: "Starter",
price: "$9/month",
features: ["5 projects", "1GB storage", "Email support"],
},
{
name: "Pro",
price: "$29/month",
features: ["Unlimited projects", "10GB storage", "Priority support", "API access"],
},
{
name: "Enterprise",
price: "Custom pricing",
features: ["Everything in Pro", "Dedicated account manager", "SSO", "99.9% SLA"],
},
];
Why define pricing as data instead of inline text?
Storing product tiers as a typed array means you can update pricing in one place and have it automatically reflected in your system prompt. It also gives you autocompletion and type checking when you reference this data elsewhere in the codebase.
- Save lib/types.ts.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
export interface ProductTier {
name: string;
price: string;
features: string[];
}
export interface OrderInfo {
orderId: string;
status: string;
product: string;
date: string;
}
export const PRODUCT_TIERS: ProductTier[] = [
{
name: "Starter",
price: "$9/month",
features: ["5 projects", "1GB storage", "Email support"],
},
{
name: "Pro",
price: "$29/month",
features: ["Unlimited projects", "10GB storage", "Priority support", "API access"],
},
{
name: "Enterprise",
price: "Custom pricing",
features: ["Everything in Pro", "Dedicated account manager", "SSO", "99.9% SLA"],
},
];
Build the system prompt with domain knowledge
A system prompt is the hidden instruction that tells the AI how to behave. Yours will define the assistant's persona, inject real product data, and set boundaries on what it can and cannot say.
- Create a new file inside lib called prompts.ts.
- Add the following system prompt that imports your product tiers and defines the Acme SaaS support persona:
import { PRODUCT_TIERS } from "./types";
export const SYSTEM_PROMPT = `You are a helpful customer support assistant for Acme SaaS, a project management platform.
You help customers with:
- Pricing and plan questions
- Feature explanations
- Troubleshooting common issues
- Account and billing inquiries
Our pricing tiers:
${PRODUCT_TIERS.map(
(tier) => `- ${tier.name} (${tier.price}): ${tier.features.join(", ")}`
).join("\n")}
Common troubleshooting:
- If a user cannot log in, suggest resetting their password at acme.example.com/reset
- If a project is not loading, suggest clearing browser cache and trying again
- If API requests are failing, check that the API key is valid and has not expired
Guidelines:
- Be concise and helpful
- If you do not know the answer, say so honestly and offer to connect them with a human agent
- Never make up information about features or pricing
- Use a friendly, professional tone`;
What does this system prompt do?
- The opening line defines who the AI is: a support agent for Acme SaaS.
- The PRODUCT_TIERS.map(...) section dynamically injects pricing data from your typed constant. If you update the tiers in types.ts, the prompt automatically reflects the change.
- The troubleshooting section gives the AI specific answers to common questions so it doesn't hallucinate solutions.
- The guidelines section sets behavioral boundaries, including the critical rule to never fabricate information.
- Save lib/prompts.ts.
Why does this fix the error?
Your route handler in app/api/chat/route.ts already imports SYSTEM_PROMPT from @/lib/prompts. That import was pointing at a file that didn't exist until now. Creating lib/prompts.ts resolves the module-not-found error and connects your structured prompt to the streaming API.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import { PRODUCT_TIERS } from "./types";
export const SYSTEM_PROMPT = `You are a helpful customer support assistant for Acme SaaS, a project management platform.
You help customers with:
- Pricing and plan questions
- Feature explanations
- Troubleshooting common issues
- Account and billing inquiries
Our pricing tiers:
${PRODUCT_TIERS.map(
(tier) => `- ${tier.name} (${tier.price}): ${tier.features.join(", ")}`
).join("\n")}
Common troubleshooting:
- If a user cannot log in, suggest resetting their password at acme.example.com/reset
- If a project is not loading, suggest clearing browser cache and trying again
- If API requests are failing, check that the API key is valid and has not expired
Guidelines:
- Be concise and helpful
- If you do not know the answer, say so honestly and offer to connect them with a human agent
- Never make up information about features or pricing
- Use a friendly, professional tone`;
Still seeing a module-not-found error?
- Make sure your lib folder is at the project root (same level as app), not inside app.
- Check that the file is named exactly types.ts (not type.ts or Types.ts). File names are case-sensitive.
- Restart your dev server if the error persists after saving both files.
Ask for help: Why am I still seeing a module not found error after creating lib/prompts.ts?
Test the assistant with domain-specific questions
With both files saved, your dev server should restart and the app should load without errors. The assistant now has structured knowledge about Acme SaaS pricing, features, and troubleshooting steps.
- Return to your browser at http://localhost:3000.
- You should see the welcome message: "Hi! I am the Acme SaaS support assistant. How can I help you today?" along with three suggested question buttons.
- Click the What pricing plans do you offer? button (or type it manually).
- Press Send and watch the response stream in.
The assistant should mention the Starter ($9/month), Pro ($29/month), and Enterprise (Custom pricing) tiers with their specific features.
What should I see?
The response should include specific details from your PRODUCT_TIERS data, like "5 projects" for Starter and "API access" for Pro. If the assistant gives vague or generic answers without mentioning these exact features, the system prompt isn't loading correctly.
- Try asking "How do I reset my password?" to test the troubleshooting knowledge.
The assistant should direct you to acme.example.com/reset because that's what the system prompt specifies.
Getting vague or generic responses?
- Open lib/prompts.ts and confirm that PRODUCT_TIERS is imported from ./types (not ../types or @/lib/types).
- Check that app/api/chat/route.ts passes system: SYSTEM_PROMPT to the streamText call.
- Make sure your .env.local file has a valid GOOGLE_GENERATIVE_AI_API_KEY value.
Ask for help: Why is my assistant giving generic responses instead of Acme SaaS-specific answers?
Your assistant now has a structured identity with real product knowledge. Next up, you'll tackle the Secret Mission and give the AI the ability to look up live order data.
Secret mission
Add Tool Calling for Order Lookup
Your support assistant can answer questions from its system prompt, but what about real-time data? In production, AI agents need access to external systems like databases and APIs. In this secret mission, you will give your assistant the ability to look up customer orders on the fly using the AI SDK's tool calling pattern.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. The Google Gemini API free tier does not require billing, and your dev server only uses resources while it is running.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The support-assistant project folder on your Desktop (contains all source code and dependencies).
- A local dev server running at http://localhost:3000.
- A Google AI Studio API key stored in .env.local.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your support assistant.
- Your dev server stays running at http://localhost:3000.
- Your project files and API key remain in place.
- The Gemini free tier has no charge, so your API key will not incur costs.
Pause - I'll come back to this later
Stop the dev server to free up your terminal and local port. Your project files stay on disk so you can resume later.
- In the terminal where the dev server is running, press Ctrl+C to stop it.
- Your support-assistant folder and all source code remain on your Desktop.
- To resume, open a terminal in the support-assistant folder and run npm run dev.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your local files and revokes your API key.
- In the terminal where the dev server is running, press Ctrl+C to stop it.
- Delete the project folder by running this command on macOS/Linux:
cd ~/Desktop
rm -rf support-assistant
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q support-assistant
- Delete your Google AI Studio API key by visiting https://aistudio.google.com/apikey. Find the key you created for this project and click the trash icon to delete it.
Nice Work!
Nice Work!
You just built a full-stack AI-powered customer support assistant from scratch using Next.js, TypeScript, and Google Gemini.
You've learned how to:
- Build a streaming API route that integrates Gemini via the Vercel AI SDK's provider pattern, making it trivial to swap AI providers with a single line change.
- Implement a real-time chat interface with the useChat hook, including loading states, error handling, and stop-generation controls.
- Design a type-safe architecture with shared types across client and server boundaries, exactly how production AI product teams structure their codebases.
- Secret Mission: Added tool calling that lets the AI look up data from a mock database, demonstrating the agentic patterns used in production AI products.
Ready to quiz yourself?