AI chatbot development that fits a real Next.js app

Table of Contents
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. This is the practice, not a hire brochure.
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.