n4nAI

How to add function calling to a Next.js AI chatbot

Step-by-step guide to adding next.js ai sdk function calling to a Next.js chatbot using the Vercel AI SDK, with code and success checks.

n4n Team3 min read737 words

Audio narration

Coming soon — every post will get a voice note here.

Adding next.js ai sdk function calling to a chatbot turns a text generator into an agent that can hit your APIs, query a database, or trigger workflows. This guide walks through a working App Router implementation using the Vercel AI SDK and a streaming chat endpoint, from scaffold to verification.

Step 1: Scaffold a Next.js App Router project

Start with a clean TypeScript App Router app. The patterns below assume the src/ directory layout and the modern app/ folder.

npx create-next-app@latest chatbot-fn --ts --app --eslint --src-dir --no-tailwind
cd chatbot-fn

Do not use the Pages Router. The Vercel AI SDK React hooks expect client components, and the App Router’s nested layouts make it easy to isolate the chat UI from the rest of your app.

Step 2: Install the Vercel AI SDK and dependencies

The SDK is modular. You need the core ai package, a provider adapter, the React bindings, and Zod for parameter schemas.

npm install ai @ai-sdk/openai @ai-sdk/react zod

The provider package here is @ai-sdk/openai, which speaks the OpenAI chat completions protocol. Because that protocol is widely implemented, you can point it at any OpenAI-compatible gateway without changing your tool code.

Step 3: Configure the model endpoint

Create .env.local and keep your key server-side:

echo "OPENAI_API_KEY=sk-..." > .env.local

If you route through n4n.ai, its OpenAI-compatible endpoint exposes 240+ models and automatically falls back when a provider is degraded, so you can swap the base URL instead of juggling multiple keys.

// src/lib/model.ts
import { createOpenAI } from '@ai-sdk/openai';

export const model = createOpenAI({
  baseURL: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
  apiKey: process.env.OPENAI_API_KEY!,
})('gpt-4o-mini');

Pick a model that actually supports function calling. Most modern instruction-tuned models do, but verify before shipping. The createOpenAI factory returns a function; call it with the model id to get a LanguageModel instance.

Step 4: Define function calling tools

Next.js ai sdk function calling is built around the tool helper. A tool is a description, a Zod schema, and an execute async function that runs on the server.

// src/lib/tools.ts
import { tool } from 'ai';
import { z } from 'zod';

export const getWeather = tool({
  parameters: z.object({
    city: z.string().describe('City name, e.g. "San Francisco"'),
  }),
  execute: async ({ city }) => {
    // Stand-in for a real HTTP call
    const tempF = city.toLowerCase().includes('sf') ? 72 : 68;
    return { city, tempF };
  },
});

The description is what the model reads to decide whether to call the tool. Be specific. “Fetch current temperature” works better than “weather”. The Zod schema is converted to JSON Schema and sent as the function signature. Your execute function must return a JSON-serializable value; the SDK serializes it and feeds it back to the model as a tool result message.

Step 5: Build the streaming API route

Create src/app/api/chat/route.ts. This is where the model and tools are wired together.

// src/app/api/chat/route.ts
import { streamText } from 'ai';
import { model } from '@/lib/model';
import { getWeather } from '@/lib/tools';

export const runtime = 'edge';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model,
    messages,
    tools: { getWeather },
    maxSteps: 3,
  });

  return result.toDataStreamResponse();
}

maxSteps is the single most common gotcha. Without it, the model emits a tool call, the SDK executes it, but the conversation stops there—no final answer. With maxSteps: 3, the SDK loops: tool call → result → model generates text or calls another tool, up to three iterations.

The Edge runtime keeps cold starts low, but if your execute uses Node-only APIs (like pg or fs), switch to export const runtime = 'nodejs'.

Step 6: Create the client chat component

Use useChat from @ai-sdk/react. It handles message state, input binding, and the EventSource stream.

// src/app/components/chat.tsx
'use client';
import { useChat } from '@ai-sdk/react';

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask about weather..."
        />
        <button type="submit">Send</button>
      </form>

      {messages.map((m) => (
        <div key={m.id} style={{ margin: '1rem 0' }}>
          <strong>{m.role}</strong>: {m.content}
          {m.toolInvocations?.map((t) => (
            <pre key={t.toolCallId}>{JSON.stringify(t, null, 2)}</pre>
          ))}
        </div>
      ))}
    </div>
  );
}

toolInvocations is populated when the model requests a tool. Rendering it raw is useful for debugging; in production you’d show a spinner or a summary instead. Mount the component in src/app/page.tsx:

import { Chat } from './components/chat';

export default function Page() {
  return (
    <main>
      <h1>Chatbot</h1>
      <Chat />
    </main>
  );
}

Step 7: Handle errors and parallel calls

The model may call the tool multiple times in one turn. The SDK runs execute functions concurrently and collects results. Write execute as a pure async function—no module-level mutable state.

Add error handling so a failed tool doesn’t crash the stream:

execute: async ({ city }) => {
  try {
    const res = await fetch(`https://api.weather.example/?q=${city}`);
    if (!res.ok) throw new Error('upstream error');
    return await res.json();
  } catch (e) {
    return { error: 'weather lookup failed' };
  }
}

The model receives the error object and can respond accordingly. For destructive operations (delete row, send email), gate execute behind a user confirmation step in the UI before calling the route.

Step 8: Verify the integration

Run the dev server:

npm run dev

Open http://localhost:3000 and send: What's the temperature in San Francisco?

A correct run shows:

  1. A POST to /api/chat with text/event-stream response in the Network tab.
  2. An assistant message with empty content and a toolInvocations entry: getWeather called with { city: "San Francisco" }.
  3. A follow-up assistant message with synthesized text using the returned temperature.

If you see the tool call but no text, maxSteps is missing. If the tool never fires, the model may not support function calling or your description is too vague.

You can also test headlessly:

curl -N -X POST http://localhost:3000/api/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"Weather in SF?"}]}'

The stream emits data: {"type":"tool-call",...} followed by data: {"type":"text-delta",...}.

Step 9: Production hardening

  • Switch runtime to nodejs if tools need Node libraries.
  • Set export const maxDuration = 30; on the route to avoid platform timeouts on multi-step chains.
  • The SDK sends prior tool results back into context. Scrub PII before logging messages.
  • If you use a gateway, rely on its per-token metering instead of writing your own middleware.

The next.js ai sdk function calling pattern above is the minimal viable agent. From here, add more tools, constrain them with tighter Zod schemas, and cache repeated tool results using provider cache-control hints if your inference layer forwards them.

Tagsnextjsvercel-ai-sdkfunction-callingchatbot

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All next.js ai chat integration (app router + vercel ai sdk) posts →