dailytutorfor.you
Web Development

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.

10 menit baca

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:

  1. Node.js 20+
  2. pnpm / npm / yarn (contoh di sini pakai npm)
  3. OpenAI API key
  4. Dasar TypeScript dan Next.js App Router
  5. Pemahaman dasar HTTP, JSON, dan environment variables

Struktur project yang akan kita buat

  • app/page.tsx → UI chat sederhana
  • app/api/agent/route.ts → Route Handler untuk agent orchestration
  • lib/openai.ts → inisialisasi OpenAI client
  • lib/tools.ts → definisi tools + eksekusi aman
  • lib/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:

  1. Kirim request ke model + daftar tools
  2. Model memutuskan perlu tool call atau tidak
  3. Server eksekusi tool call
  4. Kirim hasil tool ke model
  5. 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:

  • requestId untuk 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)

  1. Tool schema harus ketat
    Gunakan additionalProperties: false, enum jelas, required field minimal.

  2. Jangan expose secret ke client
    API key hanya di server (route.ts, server actions, backend service).

  3. Validasi semua argumen tool
    Jangan percaya output model mentah. Selalu parse + validate.

  4. Timeout dan retry policy
    Tool eksternal bisa lambat. Set timeout supaya UX tetap responsif.

  5. Observability dari hari pertama
    Simpan requestId, latency, jumlah tool call, dan error code.

  6. Fallback message yang human
    Saat tool gagal, jangan tampilkan stack trace ke user.

  7. Pisahkan orchestration vs domain logic
    route.ts untuk alur, lib/tools.ts untuk 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_docs
  • get_order_status
  • create_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:

  1. Tambah 2-3 tools domain bisnis kamu
  2. Implement auth + rate limit
  3. Tambah logging terstruktur (mis. pino)
  4. Implement streaming response
  5. Deploy + monitor latency/error rate

Ingat: AI agent yang bagus bukan yang paling “pintar”, tapi yang paling andal saat produksi.


10) References


Kalau kamu mau, di artikel lanjutan kita bisa bahas versi multi-tenant SaaS: termasuk per-organization tool permissions, billing hooks, dan audit trail lengkap.