AI chatbot development that fits a real Next.js app

Table of Contents
# AI chatbot development that fits a real Next.js app
AI chatbot development that has to live in a real Next.js app is not a chat demo. It is a route that owns the request, a stream the UI can follow, and tools the model can call without turning your app into an open proxy.
I have shipped the cute widget. A floating bubble. A third-party iframe. A key sitting in the browser because the tutorial said so. It looked finished in a walkthrough. It broke the first week someone asked the bot for something that touched a real order, a real user, or a real bill.
I’m Aris Setiawan. I ship Next.js and AI product work for client builds. I already wrote the App Router I keep in `app/`, the API routes I trust, and the cache I set on Vercel. This post is AI chatbot development when the bot has to sit in that same tree.
The offer page is Hire an AI developer.
What I wire first
I do not start with a model picker. I start with three named pieces: the route, the stream, and the tools.

Route. There is one POST that owns the chat. Usually app/api/chat/route.ts. That file reads the session, checks the user, and calls the model. I do not leave a public endpoint that any visitor can hammer with my key. If the chat is only for signed-in people, the route fails closed before a single token leaves.
Stream. I stream tokens out as they land. The user sees the answer grow. Waiting on a full JSON blob for a long reply feels broken even when the model is fine. With the Vercel AI SDK that is the normal path: the route returns a stream, the client renders it. I do not invent a second polling loop because streaming felt hard on day one.
Tools. Function calls are how the bot touches your app. getOrder. searchDocs. createTicket. Each tool has a name, a schema, and an auth check inside the handler. The model proposes the call. My code runs it. I do not hand the model a raw database client and hope.
Those three are the product. The model brand is a swap. OpenAI today, Anthropic tomorrow, something else next year. The route, the stream, and the tool list stay.
A short sketch of the route shape I keep coming back to:
// app/api/chat/route.ts
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) {
return new Response("Unauthorized", { status: 401 });
}
const { messages } = await req.json();
const result = streamText({
model,
messages,
tools: {
getOrder: {
description: "Fetch one order for the signed-in user",
parameters: orderSchema,
execute: async ({ id }) => getOrderForUser(session.user.id, id),
},
},
maxSteps: 3,
});
return result.toDataStreamResponse();
}
That is not a full product. It is the spine. Auth first. Tools that know the user. A step cap so the model cannot loop forever.
Where the bot lives in the App Router
People ask where the chat “should” live. Server Action, route handler, or a fat client component. I pick based on what the request needs to do.

Server Action when the chat is basically a form. Short prompt, one reply, same session, no need for a public URL. Fine for an internal admin helper. Awkward when you want a proper streaming UX or an external client calling in.