n4nAI

Function calling in Next.js API routes with GPT-4o

Step-by-step tutorial for implementing GPT-4o function calling inside Next.js API routes, with runnable TypeScript code and expected outputs.

n4n Team2 min read530 words

Audio narration

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

Building reliable agentic features means letting the model trigger your code. This tutorial shows how to implement nextjs api routes function calling gpt-4o with the OpenAI Node SDK and TypeScript, from project setup to returning a grounded answer.

Prerequisites

  • Node.js 18.18+ (App Router requires async request handlers, stable on 18+)
  • Next.js 14+ with the app/ directory enabled
  • An OpenAI API key, or any OpenAI-compatible key
  • Familiarity with fetch, async/await, and TypeScript interfaces

You should be able to run npx create-next-app without prompts. No prior experience with the OpenAI tools API is required, but you should know what a chat completion is.

Project setup

Scaffold a minimal TypeScript App Router project:

npx create-next-app@latest weather-agent --typescript --app --no-tailwind --no-eslint
cd weather-agent
npm install openai

The resulting structure includes app/page.tsx and app/api/. We will add three files:

app/lib/tools.ts      # tool schemas
app/lib/executors.ts  # local function implementations
app/api/chat/route.ts # the API route

Create .env.local and store the key:

OPENAI_API_KEY=sk-your-key

Never expose this key to the client. The API route is the only place it should be read.

Define the tool schemas

GPT-4o consumes tools as a list of function descriptors. Each must include a JSON Schema for its parameters. We define two tools to show how multiple functions are handled.

// app/lib/tools.ts
export const tools = [
  {
    type: "function",
    function: {
      name: "get_current_weather",
      parameters: {
        type: "object",
        properties: {
          city: { type: "string", description: "City name, e.g. 'Berlin'" },
        },
        required: ["city"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_stock_price",
      parameters: {
        type: "object",
        properties: {
          symbol: { type: "string", description: "Ticker, e.g. 'AAPL'" },
        },
        required: ["symbol"],
      },
    },
  },
] as const;

The as const keeps the names literal, which helps later with switch statements.

Implement the local executors

The model emits arguments; your server runs the real logic. Stub these with deterministic data and a forced error path for demonstration.

// app/lib/executors.ts
export async function get_current_weather(city: string): Promise<string> {
  const temps: Record<string, number> = { Berlin: 14, Paris: 18, NYC: 22 };
  if (!(city in temps)) {
    return JSON.stringify({ error: `No data for ${city}` });
  }
  return JSON.stringify({ city, temp_c: temps[city], unit: "celsius" });
}

export async function get_stock_price(symbol: string): Promise<string> {
  const prices: Record<string, number> = { AAPL: 212.4, MSFT: 438.1 };
  if (!(symbol in prices)) {
    return JSON.stringify({ error: `Unknown symbol ${symbol}` });
  }
  return JSON.stringify({ symbol, price_usd: prices[symbol] });
}

Returning a JSON string (not an object) matches what the tool message content expects.

Build the API route

This is the core of our nextjs api routes function calling gpt-4o implementation. The handler sends the user message, inspects tool_calls, executes each, and calls the model again with the results attached.

// app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { tools } from "../../lib/tools";
import { get_current_weather, get_stock_price } from "../../lib/executors";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

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

  const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
    { role: "user", content: message },
  ];

  const first = await client.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools: tools as any,
  });

  const assistantMsg = first.choices[0].message;
  messages.push(assistantMsg);

  if (!assistantMsg.tool_calls) {
    return NextResponse.json({ reply: assistantMsg.content });
  }

  for (const call of assistantMsg.tool_calls) {
    const fn = call.function;
    let result: string;
    if (fn.name === "get_current_weather") {
      const { city } = JSON.parse(fn.arguments);
      result = await get_current_weather(city);
    } else if (fn.name === "get_stock_price") {
      const { symbol } = JSON.parse(fn.arguments);
      result = await get_stock_price(symbol);
    } else {
      result = JSON.stringify({ error: "unknown tool" });
    }
    messages.push({
      role: "tool",
      tool_call_id: call.id,
      content: result,
    });
  }

  const second = await client.chat.completions.create({
    model: "gpt-4o",
    messages,
  });

  return NextResponse.json({ reply: second.choices[0].message.content });
}

Raw first-call shape

For "What is the temperature in Berlin and AAPL price?", the first response contains:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_w1",
            "type": "function",
            "function": { "name": "get_current_weather", "arguments": "{\"city\":\"Berlin\"}" }
          },
          {
            "id": "call_s1",
            "type": "function",
            "function": { "name": "get_stock_price", "arguments": "{\"symbol\":\"AAPL\"}" }
          }
        ]
      }
    }
  ]
}

Your route runs both executors, appends two tool messages, and GPT-4o returns a merged sentence.

Call the route from a client component

A minimal page that posts a hardcoded prompt and renders the reply:

// app/page.tsx
"use client";
import { useState } from "react";

export default function Home() {
  const [reply, setReply] = useState("");
  const [loading, setLoading] = useState(false);

  async function ask() {
    setLoading(true);
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        message: "What is the temperature in Berlin and AAPL price?",
      }),
    });
    const data = await res.json();
    setReply(data.reply);
    setLoading(false);
  }

  return (
    <main style={{ padding: 24 }}>
      <button onClick={ask} disabled={loading}>
        {loading ? "Asking..." : "Ask"}
      </button>
      <p>{reply}</p>
    </main>
  );
}

Test with curl

Start the dev server (npm run dev) and hit the route directly:

curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"What is the temperature in Paris?"}'

Expected JSON:

{ "reply": "The temperature in Paris is 18°C." }

If you pass an unknown city, the executor returns an error object, and GPT-4o will typically say it couldn’t retrieve the data.

Type-safe argument parsing

The raw arguments string is untrusted. Use Zod to validate before executing:

import { z } from "zod";
const WeatherArgs = z.object({ city: z.string().min(1) });

const parsed = WeatherArgs.safeParse(JSON.parse(fn.arguments));
if (!parsed.success) {
  result = JSON.stringify({ error: "invalid arguments" });
} else {
  result = await get_current_weather(parsed.data.city);
}

This prevents malformed tool calls from crashing your route.

Swapping in an OpenAI-compatible gateway

The handler above targets OpenAI’s default host. If you point the client at n4n.ai’s OpenAI-compatible endpoint, the same nextjs api routes function calling gpt-4o code runs without modification and gains automatic fallback when a provider is rate-limited or degraded, plus per-token metering:

const client = new OpenAI({
  apiKey: process.env.N4N_API_KEY,
  baseURL: "https://api.n4n.ai/v1",
});

The tools parameter and message shapes are identical because the gateway forwards to GPT-4o.

Production hardening

  • Timeouts: Wrap executor calls in Promise.race with a 5s timeout. A slow internal API should not hang the chat.
  • Idempotency: Use tool_call_id as a dedupe key if you retry the second completion.
  • Logging: Log the assistant tool_calls and your executor results. This is the fastest way to debug prompt drift.
  • Streaming: The final answer can be streamed by passing stream: true on the second call and parsing SSE on the client. The tool round-trip itself stays non-streaming.

You now have a working nextjs api routes function calling gpt-4o integration that handles multiple tools, validates input, and degrades cleanly on error.

Tagsnextjsgpt-4ofunction-callingapi-routes

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 function calling in typescript/node.js posts →