Bagian 1: Cara Membuat Chatbot dengan T3 Turbo & Gemini
Banyak founder terjebak dalam 'setup hell'. Saya baru saja membangun chatbot AI yang sepenuhnya *type-safe* hanya dalam satu sore. Berikut stack lengkapnya—Next.js, tRPC, dan Gemini—beserta kode agar Anda bisa membuatnya sendiri.

Title: Membangun Chatbot AI dengan T3 Turbo & Gemini: Panduan "Vibe Coding"
Excerpt: Kompleksitas adalah pembunuh startup. Pelajari cara membangun chatbot AI yang cepat, type-safe, dan memiliki "jiwa" menggunakan T3 Stack dan Google Gemini hanya dalam hitungan jam, bukan minggu.
Kompleksitas adalah pembunuh senyap bagi startup tahap awal. Kamu mulai dengan ide sederhana—"Saya ingin chatbot yang bicara seperti Tony Stark"—dan tiga minggu kemudian, kamu masih berkutat mengonfigurasi Webpack, bertarung dengan container Docker, atau men-debug alur autentikasi yang bahkan belum pernah digunakan oleh siapa pun.
Ini adalah jebakan yang sering saya lihat menimpa engineer berbakat berulang kali. Kita mencintai tools kita. Kita suka melakukan optimasi. Tapi dalam permainan startup, shipping (merilis produk) adalah satu-satunya metrik yang penting.
Jika kamu belum melihat ekosistem TypeScript modern akhir-akhir ini, kamu mungkin akan terkejut. Hari-hari di mana kita harus "menjahit" berbagai API yang terpisah dan berdoa agar semuanya tetap utuh sudah jauh di belakang kita. Kita telah memasuki era "Vibe Coder"—di mana jarak antara ide dan produk yang ter-deploy diukur dalam hitungan jam, bukan sprint.
Hari ini, saya akan memandu kamu melalui tech stack yang rasanya seperti cheat code: Create T3 Turbo dikombinasikan dengan Google Gemini AI. Stack ini type-safe dari database hingga frontend, sangat cepat, dan jujur saja, mengembalikan kegembiraan dalam coding.
Mengapa Stack Ini Penting
Kamu mungkin berpikir, "Feng, kenapa harus stack lain lagi? Tidak bisakah saya pakai Python dan Streamlit saja?"
Tentu, untuk prototipe. Tapi jika kamu membangun produk—sesuatu yang perlu skala besar, menangani user, dan memelihara state—kamu butuh arsitektur sungguhan. Masalahnya, "arsitektur sungguhan" biasanya berarti "berminggu-minggu menulis boilerplate."
T3 Stack (Next.js, tRPC, Tailwind) membalikkan skenario ini. Ia memberimu ketangguhan aplikasi full-stack dengan kecepatan pengembangan layaknya sebuah skrip sederhana. Ketika kamu menambahkan Drizzle ORM (ringan, mirip SQL) dan Google Gemini (cepat, free tier yang dermawan), kamu memiliki toolkit yang memungkinkan seorang solo founder mengalahkan kecepatan tim beranggotakan sepuluh orang.
Mari kita bangun sesuatu yang nyata.
Langkah 1: Setup Satu Perintah
Lupakan konfigurasi ESLint dan Prettier secara manual. Kita akan menggunakan create-t3-turbo. Ini menyiapkan struktur monorepo yang sempurna karena memisahkan logika API dari frontend Next.js kamu, sebuah investasi masa depan (future-proofing) untuk saat kamu akhirnya meluncurkan aplikasi mobile React Native nanti.
pnpm create t3-turbo@latest my-chatbot
cd my-chatbot
pnpm install
Saat ditanya, saya memilih Next.js, tRPC, dan PostgreSQL. Saya melewatkan Auth untuk saat ini karena, sekali lagi, kita mengoptimalkan untuk shipping, bukan kesempurnaan. Kamu bisa menambahkan NextAuth nanti dalam sepuluh menit.
Struktur monorepo yang kamu dapatkan:
my-chatbot/
├── apps/nextjs/ # Web app kamu
├── packages/
│ ├── api/ # Router tRPC (logika bersama)
│ ├── db/ # Skema Database + Drizzle
│ └── ui/ # Komponen bersama
Pemisahan ini berarti logika API kamu bisa digunakan ulang di web, mobile, atau bahkan aplikasi CLI. Saya pernah melihat tim membuang waktu berbulan-bulan untuk refactoring karena mereka memulai dengan menaruh segalanya dalam satu folder.
Langkah 2: Otaknya (Gemini)
OpenAI memang hebat, tapi sudahkah kamu mencoba Gemini Flash? Ini sangat cepat dan harganya agresif. Untuk antarmuka chat di mana latensi bisa membunuh vibe, kecepatan adalah sebuah fitur.
Kenapa Gemini Flash dibanding GPT-3.5/4?
- Kecepatan: ~800ms vs 2-3s waktu respons
- Biaya: 60x lebih murah dari GPT-4
- Konteks: 1M token context window (ya, satu juta)
Kita membutuhkan AI SDK untuk membuat komunikasi dengan LLM menjadi standar.
cd packages/api
pnpm add ai @ai-sdk/google
Siapkan .env kamu di root project. Jangan terlalu pusing soal database lokal; instance Postgres lokal sudah cukup.
POSTGRES_URL="postgresql://user:pass@localhost:5432/chatbot"
GOOGLE_GENERATIVE_AI_API_KEY="your_key_here"
Pro tip: Dapatkan API key Gemini kamu dari https://aistudio.google.com/app/apikey. Free tier-nya sangat dermawan sampai tidak masuk akal—60 request per menit. Kamu bakal mencapai product-market fit sebelum kamu menyentuh batas rate limit.
Langkah 3: Mendefinisikan Realitas (Skema)
Di sinilah Drizzle bersinar. Di masa lalu, kamu menulis migrasi secara manual. Sekarang, kamu mendefinisikan skema dalam TypeScript, dan database akan menurutinya.
Di packages/db/src/schema.ts, kita mendefinisikan apa itu "Message". Perhatikan bagaimana kita menggunakan drizzle-zod? Ini secara otomatis membuat skema validasi untuk API kita. Ini adalah prinsip "Don't Repeat Yourself" yang beraksi.
import { pgTable } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
// Message table for chatbot
export const Message = pgTable("message", (t) => ({
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
role: t.varchar({ length: 20 }).notNull(), // 'user' or 'assistant'
content: t.text().notNull(),
createdAt: t.timestamp().defaultNow().notNull(),
}));
// Zod schema auto-generated from table definition
export const CreateMessageSchema = createInsertSchema(Message, {
role: z.enum(["user", "assistant"]),
content: z.string().min(1).max(10000),
}).omit({ id: true, createdAt: true });
Push saja: pnpm db:push. Selesai. Database kamu sekarang sudah ada.
Apa yang baru saja terjadi? Drizzle melihat definisi TypeScript kamu dan membuat tabelnya. Tidak ada SQL yang ditulis. Tidak ada file migrasi. Inilah keajaiban dari schema-driven development.
Jika kamu ingin memverifikasi, jalankan: pnpm db:studio dan kamu akan melihat UI web di https://local.drizzle.studio dengan tabel message kamu di sana, siap menerima data.
Langkah 4: Sistem Saraf (tRPC)
Ini adalah bagian yang biasanya membuat orang tercengang. Dengan REST atau GraphQL, kamu harus mendefinisikan endpoint, tipe data, dan fetcher secara terpisah. Dengan tRPC, fungsi backend kamu adalah fungsi frontend kamu.
Kita akan membuat prosedur yang menyimpan pesan pengguna, mengambil riwayat (konteks adalah raja dalam AI), mengirimkannya ke Gemini, dan menyimpan balasannya.
Buat packages/api/src/router/chat.ts:
import type { TRPCRouterRecord } from "@trpc/server";
import { google } from "@ai-sdk/google";
import { generateText } from "ai";
import { z } from "zod/v4";
import { desc } from "@acme/db";
import { Message } from "@acme/db/schema";
import { publicProcedure } from "../trpc";
const SYSTEM_PROMPT = "You are a helpful AI assistant.";
export const chatRouter = {
sendChat: publicProcedure
.input(z.object({ content: z.string().min(1).max(10000) }))
.mutation(async ({ ctx, input }) => {
// 1. Save User Message
await ctx.db
.insert(Message)
.values({ role: "user", content: input.content });
// 2. Get Context (Last 10 messages)
const history = await ctx.db
.select()
.from(Message)
.orderBy(desc(Message.createdAt))
.limit(10);
// 3. Ask Gemini
const { text } = await generateText({
model: google("gemini-1.5-flash"),
system: SYSTEM_PROMPT,
messages: history.reverse().map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
});
// 4. Save AI Reply
return await ctx.db
.insert(Message)
.values({ role: "assistant", content: text })
.returning();
}),
getMessages: publicProcedure.query(({ ctx }) =>
ctx.db.select().from(Message).orderBy(Message.createdAt),
),
clearMessages: publicProcedure.mutation(({ ctx }) => ctx.db.delete(Message)),
} satisfies TRPCRouterRecord;
Daftarkan routernya di packages/api/src/root.ts:
import { chatRouter } from "./router/chat";
import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({
chat: chatRouter,
});
export type AppRouter = typeof appRouter;
Lihat alurnya. Linear, mudah dibaca, dan fully typed. Jika kamu mengubah skema database, kode ini akan langsung merah (error). Tidak ada kejutan saat runtime.
Kenapa ada .reverse()? Kita mengambil pesan dalam urutan menurun (terbaru dulu) tetapi LLM mengharapkan urutan kronologis (terlama dulu). Ini detail kecil yang mencegah percakapan jadi membingungkan.

Langkah 5: Antarmuka (Interface)
Di apps/nextjs/src/app/chat/page.tsx, kita hubungkan semuanya. Karena kita menggunakan tRPC, kita mendapatkan React Query secara cuma-cuma. useQuery menangani fetching, caching, dan loading state tanpa kita perlu menulis satu pun useEffect untuk pengambilan data.
(Saya menyertakan useEffect hanya untuk scroll ke bawah otomatis—karena UX itu penting).
"use client";
import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { RouterOutputs } from "@acme/api";
import { useTRPC } from "~/trpc/react";
export default function ChatPage() {
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const endRef = useRef<HTMLDivElement>(null);
const trpc = useTRPC();
const queryClient = useQueryClient();
// Automatic data fetching with caching
const { data: messages } = useQuery(trpc.chat.getMessages.queryOptions());
// Mutation with optimistic updates
const sendMsg = useMutation(
trpc.chat.sendChat.mutationOptions({
onSuccess: async () => {
await queryClient.invalidateQueries(trpc.chat.pathFilter());
setInput("");
setLoading(false);
},
onError: (err) => {
console.error("Failed:", err);
setLoading(false);
},
}),
);
// Auto-scroll to latest message
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || loading) return;
setLoading(true);
sendMsg.mutate({ content: input.trim() });
};
return (
<div className="flex h-screen flex-col bg-gray-50">
{/* Header */}
<div className="border-b bg-white p-4">
<h1 className="text-xl font-bold">AI Chat</h1>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
<div className="mx-auto max-w-4xl space-y-4">
{messages?.map((m: RouterOutputs["chat"]["getMessages"][number]) => (
<div key={m.id} className={m.role === "user" ? "text-right" : ""}>
<div
className={`inline-block rounded-2xl px-4 py-3 ${
m.role === "user"
? "bg-blue-500 text-white"
: "bg-white border shadow-sm"
}`}
>
<p className="whitespace-pre-wrap">{m.content}</p>
</div>
</div>
))}
{loading && (
<div className="flex gap-2">
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.2s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0.4s]" />
</div>
)}
<div ref={endRef} />
</div>
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="border-t bg-white p-4">
<div className="mx-auto flex max-w-4xl gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
className="flex-1 rounded-lg border px-4 py-3 focus:ring-2 focus:ring-blue-500 focus:outline-none"
disabled={loading}
/>
<button
type="submit"
disabled={!input.trim() || loading}
className="rounded-lg bg-blue-500 px-6 py-3 font-medium text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}
Jangan lupa homepage-nya. Update apps/nextjs/src/app/page.tsx:
import Link from "next/link";
export default function HomePage() {
return (
<main className="flex min-h-screen items-center justify-center bg-gradient-to-b from-blue-500 to-blue-700">
<div className="text-center text-white">
<h1 className="text-5xl font-bold">AI Chatbot</h1>
<p className="mt-4 text-xl">Built with T3 Turbo + Gemini</p>
<Link
href="/chat"
className="mt-8 inline-block rounded-full bg-white px-10 py-3 font-semibold text-blue-600 hover:bg-gray-100 transition"
>
Start Chatting
</Link>
</div>
</main>
);
}
Jalankan pnpm dev dan kunjungi http://localhost:3000. Klik "Start Chatting" dan kamu punya chatbot AI yang berfungsi.
Keajaiban tRPC: Perhatikan bagaimana kita tidak pernah menulis API fetch? Tidak ada panggilan fetch(), tidak ada string URL, tidak ada penanganan error manual. TypeScript tahu apa yang diharapkan oleh sendMsg.mutate(). Jika kamu mengubah skema input backend, frontend kamu akan melemparkan error kompilasi. Inilah masa depan.
Langkah 6: Menyuntikkan Jiwa (Cek "Vibe")
Asisten generik itu membosankan. Asisten generik bakal dihapus. Keindahan LLM adalah mereka sangat jago bermain peran (role-play).
Saya menemukan bahwa memberikan bot kamu opini yang kuat membuatnya 10x lebih menarik. Jangan hanya memberi prompt "Kamu sangat membantu." Berikan prompt untuk sebuah kepribadian.
Mari kita modifikasi backend untuk menerima persona. Update packages/api/src/router/chat.ts:
const PROMPTS = {
default: "You are a helpful AI assistant. Be concise and clear.",
luffy:
"You are Monkey D. Luffy from One Piece. You're energetic, optimistic, love meat and adventure. You often say 'I'm gonna be King of the Pirates!' Speak simply and enthusiastically.",
stark:
"You are Tony Stark (Iron Man). You're a genius inventor, witty, and sarcastic. You love technology and often mention Stark Industries. Call people 'kid' or 'buddy'. Be charming but arrogant.",
};
export const chatRouter = {
sendChat: publicProcedure
.input(
z.object({
content: z.string().min(1).max(10000),
character: z.enum(["default", "luffy", "stark"]).optional(),
}),
)
.mutation(async ({ ctx, input }) => {
// Pick the personality
const systemPrompt = PROMPTS[input.character || "default"];
await ctx.db
.insert(Message)
.values({ role: "user", content: input.content });
const history = await ctx.db
.select()
.from(Message)
.orderBy(desc(Message.createdAt))
.limit(10);
const { text } = await generateText({
model: google("gemini-1.5-flash"),
system: systemPrompt, // ← Dynamic prompt
messages: history.reverse().map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
});
return await ctx.db
.insert(Message)
.values({ role: "assistant", content: text })
.returning();
}),
// ... rest stays the same
};
Update frontend untuk mengirim pilihan karakter:
// In ChatPage component, add state for character
const [character, setCharacter] = useState<"default" | "luffy" | "stark">("default");
// Update the mutation call
sendMsg.mutate({ content: input.trim(), character });
// Add a dropdown before the input:
<select
value={character}
onChange={(e) => setCharacter(e.target.value as any)}
className="rounded-lg border px-3 py-2"
>
<option value="default">🤖 Default</option>
<option value="luffy">👒 Luffy</option>
<option value="stark">🦾 Tony Stark</option>
</select>
Sekarang kamu bukan hanya membangun chatbot; kamu telah membangun platform interaksi karakter. Itu adalah sebuah produk.
Detail Teknis yang Sebenarnya Kamu Pedulikan
Kenapa tidak pakai Prisma saja?
Prisma itu bagus, tapi Drizzle lebih cepat. Kita bicara soal performa query 2-3x lipat. Saat kamu adalah solo founder, setiap milidetik itu berakumulasi. Ditambah lagi, sintaks Drizzle yang mirip SQL berarti beban mental (mental overhead) yang lebih sedikit.
Bagaimana dengan streaming response?
Vercel AI SDK mendukung streaming secara bawaan. Ganti generateText dengan streamText dan gunakan hook useChat di frontend. Saya melewatkannya di sini karena untuk tutorial, request/response lebih sederhana. Tapi di production? Stream semuanya. User merasa streaming itu "lebih cepat" bahkan ketika total waktunya sama.
Manajemen context window
Saat ini kita mengambil 10 pesan terakhir. Itu berhasil sampai akhirnya tidak lagi. Jika kamu membangun produk serius, implementasikan penghitung token dan sesuaikan riwayat secara dinamis. AI SDK punya utilitas untuk ini.
import { anthropic } from "@ai-sdk/anthropic";
const { text } = await generateText({
model: anthropic("claude-3-5-sonnet-20241022"),
maxTokens: 1000, // Control costs
// ...
});
Database connection pooling
Postgres lokal oke untuk dev. Untuk production, gunakan Vercel Postgres atau Supabase. Mereka menangani connection pooling secara otomatis. Serverless + koneksi database adalah jebakan—jangan mengelolanya sendiri.
Pelajaran Praktis
Jika kamu membaca ini dan merasa gatal ingin ngoding, ini saran saya:
- Jangan mulai dari nol. Boilerplate adalah musuh momentum. Gunakan T3 Turbo atau scaffolding serupa.
- Type safety adalah kecepatan. Rasanya lebih lambat di satu jam pertama, tapi lebih cepat untuk sepuluh tahun ke depan. Ia menangkap bug yang biasanya muncul saat demo.
- Konteks adalah kunci. Chatbot tanpa riwayat hanyalah search bar yang mewah. Selalu kirim beberapa pesan terakhir ke LLM.
- Kepribadian > fitur. Bot yang terdengar seperti Tony Stark akan mendapatkan interaksi lebih tinggi daripada bot generik dengan 10 fitur tambahan.
Realitas yang Berantakan
Membangun ini tidak semuanya mulus. Awalnya saya mengacaukan string koneksi database dan menghabiskan 20 menit bertanya-tanya kenapa Drizzle "meneriaki" saya. Saya juga kena rate limit di Gemini karena awalnya mengirim terlalu banyak riwayat (pelajaran: selalu mulai dengan .limit(5) dan tingkatkan perlahan).
Animasi loading? Itu butuh tiga kali percobaan sampai benar karena animasi CSS entah bagaimana masih terasa seperti ilmu hitam di tahun 2024.
Tapi begini: karena saya menggunakan stack yang tangguh, itu semua adalah masalah logika, bukan masalah struktural. Fondasinya tetap kokoh. Saya tidak pernah harus melakukan refactor seluruh API karena salah memilih abstraksi.
Ship It
Kita hidup di zaman keemasan untuk membangun sesuatu. Tools-nya sangat kuat, AI-nya cerdas, dan hambatan untuk masuk (barrier to entry) belum pernah serendah ini.
Kamu sudah punya kodenya sekarang. Kamu punya stack-nya. Kamu mengerti trade-off-nya.
Pergilah bangun sesuatu yang seharusnya tidak ada, dan rilis (ship it) sebelum makan malam.
Total waktu build: ~2 jam Baris kode aktual yang ditulis: ~200 Bug yang ditemui di production: 0 (sejauh ini)
T3 stack + Gemini bukan hanya cepat—ini membosankan dalam artian terbaik. Tidak ada kejutan. Tidak ada "works on my machine." Hanya membangun.
Selamat ngoding.
Resources:
Full code: github.com/giftedunicorn/my-chatbot
Bagikan ini

Feng Liu
shenjian8628@gmail.com