Tutorial Lengkap Next.js 16 + OpenAI Tool Calling: Membangun AI Agent Web yang Production-Ready (2026)
Pelajari cara membangun AI agent web modern dengan Next.js 16, Route Handlers, dan OpenAI tool calling. Tutorial ini membahas arsitektur, implementasi end-to-end, best practices, error handling, hingga deployment checklist untuk aplikasi production.
Tutorial Lengkap Next.js 16 + OpenAI Tool Calling: Membangun AI Agent Web yang Production-Ready (2026)
Level: Menengah ke Lanjut
Estimasi baca: 15 menit
Stack: Next.js 16 (App Router), TypeScript, OpenAI Responses API, Tool Calling
1) Introduction — What and Why
Kalau kamu aktif lihat tren developer beberapa bulan terakhir, ada pola yang jelas: AI agent bukan lagi sekadar chatbot tanya-jawab. Sekarang, agent dipakai untuk mengambil aksi nyata seperti membaca data, memanggil API internal, menjalankan workflow, dan membantu keputusan operasional.
Di GitHub Trending, banyak repo bertema coding agent, AI workflow, dan tool orchestration. Di dev.to juga ramai artikel tentang AI agent architecture, guardrails, dan “vibe coding” yang dihubungkan ke aplikasi web nyata. Artinya: pasar butuh engineer yang tidak cuma bisa “prompting”, tapi bisa membangun sistem AI yang reliable, aman, dan maintainable.
Tutorial ini fokus ke use case yang realistis:
- User bertanya ke asisten AI dalam aplikasi web
- Model boleh memanggil tools tertentu (misalnya cek cuaca, kalkulasi ongkir, cek stok)
- Server mengeksekusi tool secara aman
- Hasil tool dikembalikan ke model untuk jawaban final
Analogi sederhananya: model itu otak strategis, tools itu tangan dan kaki. Tanpa tools, model hanya “berpikir”. Dengan tools, model bisa “bertindak”.
Di akhir tutorial, kamu akan punya kerangka AI agent web yang bisa kamu pakai sebagai fondasi produk SaaS, internal dashboard, atau customer support assistant.
2) Prerequisites
Sebelum mulai, pastikan kamu punya:
- Node.js 20+
- pnpm / npm / yarn (contoh di sini pakai npm)
- OpenAI API key
- Dasar TypeScript dan Next.js App Router
- Pemahaman dasar HTTP, JSON, dan environment variables
Struktur project yang akan kita buat
app/page.tsx→ UI chat sederhanaapp/api/agent/route.ts→ Route Handler untuk agent orchestrationlib/openai.ts→ inisialisasi OpenAI clientlib/tools.ts→ definisi tools + eksekusi amanlib/schemas.ts→ validasi input/output
3) Core Concepts
Sebelum ngoding, pahami dulu konsep inti berikut.
A. Tool Calling Flow (5 langkah)
Sesuai pola dokumentasi OpenAI function/tool calling:
- Kirim request ke model + daftar tools
- Model memutuskan perlu tool call atau tidak
- Server eksekusi tool call
- Kirim hasil tool ke model
- Model menghasilkan jawaban final
B. Route Handler di Next.js
Next.js App Router menyediakan route.ts untuk handler GET/POST/... berbasis Web Request/Response API. Ini cocok untuk endpoint AI karena:
- mudah menerima payload JSON
- mudah set status code dan headers
- bisa jalan di Node runtime
C. Guardrails
Agent yang baik itu bukan yang “bebas”, tapi yang terkendali. Guardrails minimal:
- daftar tools terbatas (allowlist)
- validasi argumen tool
- timeout tool
- logging aman (tanpa bocorkan secret)
- fallback response saat tool gagal
D. Idempotency dan Observability
Di production, request bisa retry. Kamu perlu:
requestIduntuk tracing- log terstruktur
- tangani error model/tool dengan jelas
4) Architecture / Diagram
Berikut arsitektur sederhana tapi production-minded:
+------------------+ 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
Prinsip penting: model tidak pernah langsung akses sistem sensitif. Semua lewat server kamu.
5) Step-by-Step Implementation (Complete Runnable Code)
Step 1 — Inisialisasi project
npx create-next-app@latest ai-agent-next --ts --app --eslint cd ai-agent-next npm install openai zod
Buat .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 belum diset di 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 dan lib/tools.ts)
// lib/schemas.ts import { z } from "zod"; export const UserMessageSchema = z.object({ message: z.string().min(1, "Pesan tidak boleh kosong").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: simulasi call ke weather provider eksternal // Di production, ganti dengan fetch ke API cuaca sungguhan. 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: "Ambil cuaca terkini berdasarkan nama kota", parameters: { type: "object", properties: { city: { type: "string", description: "Nama kota, contoh: Surabaya", }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "Satuan suhu", }, }, required: ["city"], additionalProperties: false, }, strict: true, }, ]; export async function executeTool(name: string, rawArgs: unknown) { if (name !== "get_weather") { throw new Error(`Tool tidak diizinkan: ${name}`); } const parsed = WeatherArgsSchema.safeParse(rawArgs); if (!parsed.success) { throw new Error(`Argumen tool tidak valid: ${parsed.error.message}`); } return withTimeout(getWeather(parsed.data), TOOL_TIMEOUT_MS); }
Step 4 — Route Handler agent (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: "Payload tidak valid", details: parsed.error.flatten(), }, { status: 400 } ); } const { message, requestId = crypto.randomUUID() } = parsed.data; // 1) Call model dengan tool definitions const first = await openai.responses.create({ model: DEFAULT_MODEL, input: [ { role: "system", content: "Kamu asisten yang membantu pengguna dalam Bahasa Indonesia. Gunakan tool jika memang diperlukan.", }, { role: "user", content: message }, ], tools: TOOL_DEFINITIONS, }); // 2) Cari apakah ada tool call const toolCalls = (first.output || []).filter((item: any) => item.type === "function_call"); // Jika tidak ada tool call, langsung return if (toolCalls.length === 0) { return NextResponse.json({ ok: true, requestId, answer: first.output_text || "Maaf, saya belum bisa menjawab.", latencyMs: Date.now() - startedAt, }); } // 3) Eksekusi tool call satu per satu (serial untuk kontrol) 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) Kirim balik tool output ke model untuk final response const second = await openai.responses.create({ model: DEFAULT_MODEL, input: [...(first.output || []), ...toolOutputs], tools: TOOL_DEFINITIONS, }); // 5) Return jawaban final return NextResponse.json({ ok: true, requestId, answer: second.output_text || "Proses selesai, tapi belum ada teks jawaban.", toolCallsCount: toolCalls.length, latencyMs: Date.now() - startedAt, }); } catch (err) { return NextResponse.json( { ok: false, error: err instanceof Error ? err.message : "Terjadi error internal", }, { status: 500 } ); } }
Step 5 — UI sederhana (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("Bagaimana cuaca di Surabaya hari ini?"); 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>Contoh agent dengan 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 ? "Memproses..." : "Kirim"} </button> </form> {result && ( <section style={{ marginTop: 20, padding: 12, border: "1px solid #ddd" }}> <h2>Hasil</h2> <pre style={{ whiteSpace: "pre-wrap" }}> {JSON.stringify(result, null, 2)} </pre> </section> )} </main> ); }
Step 6 — Jalankan lokal
npm run dev
Buka http://localhost:3000, lalu tes prompt:
- “Bagaimana cuaca di Surabaya hari ini?”
- “Berikan saran outfit berdasarkan cuaca di Bandung.”
6) Best Practices (Tips Industri)
-
Tool schema harus ketat
GunakanadditionalProperties: false, enum jelas, required field minimal. -
Jangan expose secret ke client
API key hanya di server (route.ts, server actions, backend service). -
Validasi semua argumen tool
Jangan percaya output model mentah. Selalu parse + validate. -
Timeout dan retry policy
Tool eksternal bisa lambat. Set timeout supaya UX tetap responsif. -
Observability dari hari pertama
SimpanrequestId, latency, jumlah tool call, dan error code. -
Fallback message yang human
Saat tool gagal, jangan tampilkan stack trace ke user. -
Pisahkan orchestration vs domain logic
route.tsuntuk alur,lib/tools.tsuntuk bisnis logic.
7) Common Mistakes (dan Cara Menghindarinya)
Mistake #1: Membiarkan model memanggil tool apa pun
Tanpa allowlist, risiko keamanan naik drastis. Solusi: hardcode mapping tool yang valid.
Mistake #2: Tidak handle JSON parse error
Kadang argumen tool tidak valid JSON. Solusi: try/catch khusus parse.
Mistake #3: Menganggap 1 request = 1 response final
Dalam tool calling, bisa ada beberapa step. Solusi: desain endpoint yang siap multi-turn internal.
Mistake #4: Tidak memisahkan error user vs error system
Payload invalid harus 400, internal failure 500, timeout bisa 504 bila perlu.
Mistake #5: Logging berlebihan
Jangan log PII atau secret. Terapkan redaction.
8) Advanced Tips (Untuk yang Mau Lebih Dalam)
A. Multi-tool orchestration
Kamu bisa tambah tools seperti:
search_docsget_order_statuscreate_support_ticket
Gunakan strategi serial dulu (lebih aman), lalu optimasi paralel kalau sudah stabil.
B. Streaming response ke UI
Untuk UX lebih halus, gunakan streaming (SSE/ReadableStream) agar user lihat jawaban bertahap.
C. Policy layer
Tambahkan layer kebijakan sebelum eksekusi tool:
- role-based access
- rate limit per user
- quota per organisasi
D. Caching
Untuk tool dengan data tidak real-time (misal dokumentasi), cache hasil 1-5 menit agar biaya lebih efisien.
E. Test strategy
Minimal punya:
- unit test untuk validator schema
- integration test untuk endpoint
/api/agent - contract test untuk tool I/O
9) Summary and Next Steps
Kita sudah membangun AI agent web modern dengan pola production-ready:
- Next.js Route Handler sebagai orchestration layer
- OpenAI tool calling flow 2 tahap (request awal + tool output)
- Validasi ketat dengan Zod
- Error handling, timeout, dan response terstruktur
Kalau kamu ingin lanjut, urutan belajar terbaik:
- Tambah 2-3 tools domain bisnis kamu
- Implement auth + rate limit
- Tambah logging terstruktur (mis. pino)
- Implement streaming response
- Deploy + monitor latency/error rate
Ingat: AI agent yang bagus bukan yang paling “pintar”, tapi yang paling andal saat produksi.
10) References
- Next.js Route Handlers / route.ts docs: https://nextjs.org/docs/app/api-reference/file-conventions/route
- OpenAI Function/Tool Calling Guide: https://developers.openai.com/api/docs/guides/function-calling
- OpenAI Node SDK (official): https://github.com/openai/openai-node
- Vercel AI Chatbot template: https://github.com/vercel/ai-chatbot
- Vercel AI SDK docs: https://ai-sdk.dev/docs/introduction
- Next.js docs (App Router): https://nextjs.org/docs
Kalau kamu mau, di artikel lanjutan kita bisa bahas versi multi-tenant SaaS: termasuk per-organization tool permissions, billing hooks, dan audit trail lengkap.