dailytutorfor.you
Web Development

Complete Next.js 16 + OpenAI Tool Calling Tutorial: Building a Production-Ready AI Agent Web App (2026)

Learn how to build a modern AI agent web app with Next.js 16, Route Handlers, and OpenAI tool calling. This tutorial covers architecture, end-to-end implementation, best practices, error handling, and a deployment checklist for production applications.

10 min read

Complete Next.js 16 + OpenAI Tool Calling Tutorial: Building a Production-Ready AI Agent Web App (2026)

Level: Intermediate to Advanced
Estimated read: 15 minutes
Stack: Next.js 16 (App Router), TypeScript, OpenAI Responses API, Tool Calling

1) Introduction — What and Why

If you've been following developer trends over the last few months, there's a clear pattern: AI agents are no longer just Q&A chatbots. Agents are now used to take real actions such as reading data, calling internal APIs, running workflows, and helping with operational decisions.

On GitHub Trending, many repositories focus on coding agents, AI workflows, and tool orchestration. On dev.to, articles about AI agent architecture, guardrails, and “vibe coding” connected to real web applications are also booming. This means the market needs engineers who can do more than just “prompting”—they need to build AI systems that are reliable, secure, and maintainable.

This tutorial focuses on a realistic use case:

  • A user asks an AI assistant inside a web application
  • The model is allowed to call specific tools (for example: weather checks, shipping-cost calculations, stock checks)
  • The server executes the tool safely
  • The tool result is returned to the model for the final answer

A simple analogy: the model is the strategic brain, and tools are the hands and feet. Without tools, the model only “thinks.” With tools, the model can “act.”

At the end of this tutorial, you'll have a web AI agent framework you can use as the foundation for a SaaS product, an internal dashboard, or a customer support assistant.


2) Prerequisites

Before starting, make sure you have:

  1. Node.js 20+
  2. pnpm / npm / yarn (examples here use npm)
  3. OpenAI API key
  4. Basic TypeScript and Next.js App Router knowledge
  5. Basic understanding of HTTP, JSON, and environment variables

Project structure we will build

  • app/page.tsx → simple chat UI
  • app/api/agent/route.ts → Route Handler for agent orchestration
  • lib/openai.ts → OpenAI client initialization
  • lib/tools.ts → tool definitions + safe execution
  • lib/schemas.ts → input/output validation

3) Core Concepts

Before coding, understand these key concepts first.

A. Tool Calling Flow (5 steps)

Following the OpenAI function/tool calling documentation pattern:

  1. Send a request to the model + tool list
  2. The model decides whether a tool call is needed
  3. The server executes the tool call
  4. Send the tool result back to the model
  5. The model generates the final answer

B. Route Handler in Next.js

Next.js App Router provides route.ts for GET/POST/... handlers based on the Web Request/Response API. This is ideal for AI endpoints because:

  • easy to receive JSON payloads
  • easy to set status codes and headers
  • can run in the Node runtime

C. Guardrails

A good agent is not the one that is “free,” but one that is controlled. Minimum guardrails:

  • limited tool list (allowlist)
  • tool argument validation
  • tool timeout
  • safe logging (without leaking secrets)
  • fallback response when a tool fails

D. Idempotency and Observability

In production, requests can be retried. You need:

  • requestId for tracing
  • structured logs
  • clear model/tool error handling

4) Architecture / Diagram

Here is a simple yet production-minded architecture:

+------------------+ POST /api/agent +----------------------+ | Browser Client | ----------------------------> | Next.js RouteHandler | | (Chat UI) | | app/api/agent/route | +------------------+ +----------+-----------+ | | 1) call model + tool schemas v +-------------------+ | OpenAI Responses | | API | +---------+---------+ | if tool_call | v +--------------------+ | Tool Executor | | (safe allowlist) | +---------+----------+ | | 2) run tool (HTTP/API/DB) v +--------------------+ | External Service | | (example: weather) | +--------------------+ Then: - Tool output -> back to OpenAI - OpenAI final output -> Next.js -> Browser

Key principle: the model never directly accesses sensitive systems. Everything goes through your server.


5) Step-by-Step Implementation (Complete Runnable Code)

Step 1 — Initialize the project

npx create-next-app@latest ai-agent-next --ts --app --eslint cd ai-agent-next npm install openai zod

Create .env.local:

OPENAI_API_KEY=sk-xxxx OPENAI_MODEL=gpt-5.2

Step 2 — OpenAI client (lib/openai.ts)

// lib/openai.ts import OpenAI from "openai"; const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OPENAI_API_KEY is not set in the environment"); } export const openai = new OpenAI({ apiKey }); export const DEFAULT_MODEL = process.env.OPENAI_MODEL ?? "gpt-5.2";

Step 3 — Schema + tools (lib/schemas.ts and lib/tools.ts)

// lib/schemas.ts import { z } from "zod"; export const UserMessageSchema = z.object({ message: z.string().min(1, "Message cannot be empty").max(4000), requestId: z.string().optional(), }); export const WeatherArgsSchema = z.object({ city: z.string().min(2).max(80), unit: z.enum(["celsius", "fahrenheit"]).default("celsius"), }); export type WeatherArgs = z.infer<typeof WeatherArgsSchema>;
// lib/tools.ts import { z } from "zod"; import { WeatherArgsSchema, type WeatherArgs } from "./schemas"; const TOOL_TIMEOUT_MS = 8000; function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("Tool timeout")), ms); promise .then((value) => { clearTimeout(timer); resolve(value); }) .catch((err) => { clearTimeout(timer); reject(err); }); }); } async function getWeather(args: WeatherArgs) { // Demo: simulation of an external weather provider call // In production, replace this with a fetch call to a real weather API. const fakeTemp = args.unit === "celsius" ? 30 : 86; return { city: args.city, unit: args.unit, temperature: fakeTemp, condition: "Partly Cloudy", source: "demo-weather-provider", fetchedAt: new Date().toISOString(), }; } export const TOOL_DEFINITIONS = [ { type: "function" as const, name: "get_weather", description: "Get the current weather based on city name", parameters: { type: "object", properties: { city: { type: "string", description: "City name, for example: Surabaya", }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "Temperature unit", }, }, required: ["city"], additionalProperties: false, }, strict: true, }, ]; export async function executeTool(name: string, rawArgs: unknown) { if (name !== "get_weather") { throw new Error(`Tool not allowed: ${name}`); } const parsed = WeatherArgsSchema.safeParse(rawArgs); if (!parsed.success) { throw new Error(`Invalid tool arguments: ${parsed.error.message}`); } return withTimeout(getWeather(parsed.data), TOOL_TIMEOUT_MS); }

Step 4 — Agent Route Handler (app/api/agent/route.ts)

// app/api/agent/route.ts import { NextResponse } from "next/server"; import { openai, DEFAULT_MODEL } from "@/lib/openai"; import { TOOL_DEFINITIONS, executeTool } from "@/lib/tools"; import { UserMessageSchema } from "@/lib/schemas"; export const runtime = "nodejs"; export async function POST(req: Request) { const startedAt = Date.now(); try { const json = await req.json(); const parsed = UserMessageSchema.safeParse(json); if (!parsed.success) { return NextResponse.json( { ok: false, error: "Invalid payload", details: parsed.error.flatten(), }, { status: 400 } ); } const { message, requestId = crypto.randomUUID() } = parsed.data; // 1) Call model with tool definitions const first = await openai.responses.create({ model: DEFAULT_MODEL, input: [ { role: "system", content: "You are an assistant that helps users in Indonesian. Use tools only when necessary.", }, { role: "user", content: message }, ], tools: TOOL_DEFINITIONS, }); // 2) Check whether there are tool calls const toolCalls = (first.output || []).filter((item: any) => item.type === "function_call"); // If no tool call, return immediately if (toolCalls.length === 0) { return NextResponse.json({ ok: true, requestId, answer: first.output_text || "Sorry, I cannot answer yet.", latencyMs: Date.now() - startedAt, }); } // 3) Execute tool calls one by one (serial for control) const toolOutputs: any[] = []; for (const call of toolCalls) { try { const args = JSON.parse(call.arguments || "{}"); const result = await executeTool(call.name, args); toolOutputs.push({ type: "function_call_output", call_id: call.call_id, output: JSON.stringify({ ok: true, result }), }); } catch (toolError) { toolOutputs.push({ type: "function_call_output", call_id: call.call_id, output: JSON.stringify({ ok: false, error: toolError instanceof Error ? toolError.message : "Unknown tool error", }), }); } } // 4) Send tool outputs back to model for final response const second = await openai.responses.create({ model: DEFAULT_MODEL, input: [...(first.output || []), ...toolOutputs], tools: TOOL_DEFINITIONS, }); // 5) Return final answer return NextResponse.json({ ok: true, requestId, answer: second.output_text || "Process finished, but no answer text is available yet.", toolCallsCount: toolCalls.length, latencyMs: Date.now() - startedAt, }); } catch (err) { return NextResponse.json( { ok: false, error: err instanceof Error ? err.message : "Internal error occurred", }, { status: 500 } ); } }

Step 5 — Simple UI (app/page.tsx)

"use client"; import { FormEvent, useState } from "react"; type AgentResponse = { ok: boolean; answer?: string; error?: string; requestId?: string; latencyMs?: number; }; export default function HomePage() { const [message, setMessage] = useState("How is the weather in Surabaya today?"); const [loading, setLoading] = useState(false); const [result, setResult] = useState<AgentResponse | null>(null); async function onSubmit(e: FormEvent) { e.preventDefault(); setLoading(true); setResult(null); try { const res = await fetch("/api/agent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), }); const data: AgentResponse = await res.json(); setResult(data); } catch (error) { setResult({ ok: false, error: error instanceof Error ? error.message : "Network error", }); } finally { setLoading(false); } } return ( <main style={{ maxWidth: 760, margin: "40px auto", fontFamily: "sans-serif" }}> <h1>Next.js AI Agent Demo</h1> <p>Example agent with tool calling + error handling.</p> <form onSubmit={onSubmit} style={{ display: "grid", gap: 8 }}> <textarea value={message} onChange={(e) => setMessage(e.target.value)} rows={4} style={{ width: "100%", padding: 12 }} /> <button type="submit" disabled={loading}> {loading ? "Processing..." : "Send"} </button> </form> {result && ( <section style={{ marginTop: 20, padding: 12, border: "1px solid #ddd" }}> <h2>Result</h2> <pre style={{ whiteSpace: "pre-wrap" }}> {JSON.stringify(result, null, 2)} </pre> </section> )} </main> ); }

Step 6 — Run locally

npm run dev

Open http://localhost:3000, then test prompts:

  • “How is the weather in Surabaya today?”
  • “Give me outfit suggestions based on the weather in Bandung.”

6) Best Practices (Industry Tips)

  1. Tool schema must be strict
    Use additionalProperties: false, clear enums, and minimal required fields.

  2. Do not expose secrets to the client
    Keep API keys server-side only (route.ts, server actions, backend service).

  3. Validate all tool arguments
    Never trust raw model output. Always parse + validate.

  4. Timeout and retry policy
    External tools can be slow. Set timeouts to keep UX responsive.

  5. Observability from day one
    Store requestId, latency, tool call count, and error codes.

  6. Human-friendly fallback messages
    When tools fail, don’t show stack traces to users.

  7. Separate orchestration vs domain logic
    Use route.ts for flow, lib/tools.ts for business logic.


7) Common Mistakes (and How to Avoid Them)

Mistake #1: Letting the model call any tool

Without an allowlist, security risk rises drastically. Solution: hardcode valid tool mappings.

Mistake #2: Not handling JSON parse errors

Sometimes tool arguments are not valid JSON. Solution: dedicated try/catch for parsing.

Mistake #3: Assuming 1 request = 1 final response

In tool calling, there can be multiple steps. Solution: design endpoints ready for internal multi-turn flow.

Mistake #4: Not separating user errors vs system errors

Invalid payload should return 400, internal failure 500, and timeout can be 504 when needed.

Mistake #5: Excessive logging

Do not log PII or secrets. Apply redaction.


8) Advanced Tips (For Those Who Want to Go Deeper)

A. Multi-tool orchestration

You can add tools like:

  • search_docs
  • get_order_status
  • create_support_ticket

Use a serial strategy first (safer), then optimize in parallel once stable.

B. Streaming responses to the UI

For smoother UX, use streaming (SSE/ReadableStream) so users can see answers gradually.

C. Policy layer

Add a policy layer before tool execution:

  • role-based access
  • rate limiting per user
  • quota per organization

D. Caching

For tools with non-real-time data (for example documentation), cache results for 1–5 minutes to improve cost efficiency.

E. Test strategy

At minimum, have:

  • unit tests for schema validators
  • integration tests for /api/agent endpoint
  • contract tests for tool I/O

9) Summary and Next Steps

We have built a modern web AI agent with a production-ready pattern:

  • Next.js Route Handler as the orchestration layer
  • Two-stage OpenAI tool calling flow (initial request + tool output)
  • Strict validation with Zod
  • Error handling, timeout, and structured response

If you want to continue, the best learning sequence is:

  1. Add 2–3 tools for your business domain
  2. Implement auth + rate limiting
  3. Add structured logging (e.g., pino)
  4. Implement streaming responses
  5. Deploy + monitor latency/error rate

Remember: a great AI agent is not the one that is the smartest, but the one that is the most reliable in production.


10) References


If you want, in the follow-up article we can cover the multi-tenant SaaS version, including per-organization tool permissions, billing hooks, and a complete audit trail.