Bölüm 1: T3 Turbo ve Gemini ile Chatbot Nasıl Yapılır?
Çoğu kurucu 'kurulum cehenneminde' boğulurken, ben tek bir öğleden sonrada tamamen tip güvenli (type-safe) bir AI chatbot inşa ettim. İşte kullandığım stack—Next.js, tRPC ve Gemini—ve hemen işe koyulmanız için gereken tüm kodlar.

Karmaşıklık, erken aşama startup'ların sessiz katilidir. Basit bir fikirle yola çıkarsınız—"Tony Stark gibi konuşan bir chatbot istiyorum"—ve üç hafta sonra hala Webpack yapılandırmasıyla uğraşıyor, Docker konteynerleriyle savaşıyor ya da henüz kimsenin kullanmadığı bir kimlik doğrulama akışını (auth flow) debug ediyorsunuzdur.
Bu, son derece yetenekli mühendislerin tekrar tekrar düştüğünü gördüğüm bir tuzak. Araçlarımızı seviyoruz. Optimize etmeyi seviyoruz. Ancak startup oyununda, ürünü canlıya almak (shipping) önemli olan tek metriktir.
Eğer son zamanlarda modern TypeScript ekosistemine bakmadıysanız, şaşırabilirsiniz. Farklı API'leri birbirine yamayıp bir arada durmaları için dua ettiğimiz günler büyük ölçüde geride kaldı. Artık bir fikir ile canlıdaki ürün arasındaki mesafenin sprintlerle değil, saatlerle ölçüldüğü "Vibe Coder" çağına girdik.
Bugün size hile kodu gibi hissettiren bir teknoloji yığınını (stack) anlatacağım: Google Gemini AI ile birleştirilmiş Create T3 Turbo. Veritabanından ön yüze (frontend) kadar tip güvenli (type-safe), gülünç derecede hızlı ve dürüst olmak gerekirse, kodlamaya neşeyi geri getiriyor.
Bu Stack Neden Önemli?
Şöyle düşünüyor olabilirsiniz: "Feng, neden başka bir stack daha? Sadece Python ve Streamlit kullanamaz mıyım?"
Elbette, bir prototip için kullanabilirsiniz. Ancak ölçeklenmesi gereken, kullanıcıları yöneten ve durum (state) koruyan bir ürün inşa ediyorsanız, gerçek bir mimariye ihtiyacınız vardır. Sorun şu ki, "gerçek mimari" genellikle "haftalarca süren şablon kod (boilerplate)" anlamına gelir.
T3 Stack (Next.js, tRPC, Tailwind) bu senaryoyu tersine çeviriyor. Size bir script yazma hızında, full-stack bir uygulamanın sağlamlığını sunuyor. Buna Drizzle ORM (hafif, SQL benzeri) ve Google Gemini'yi (hızlı, cömert ücretsiz katman) eklediğinizde, tek kişilik bir kurucunun on kişilik bir takımı geride bırakmasını sağlayan bir araç setine sahip olursunuz.
Hadi gerçek bir şeyler inşa edelim.
Adım 1: Tek Komutla Kurulum
ESLint ve Prettier'ı elle yapılandırmayı unutun. create-t3-turbo kullanacağız. Bu, API mantığınızı Next.js ön yüzünüzden ayıran bir monorepo yapısı kurar; bu da sizi ileride kaçınılmaz olarak bir React Native mobil uygulaması geliştireceğiniz günler için geleceğe hazırlar.
pnpm create t3-turbo@latest my-chatbot
cd my-chatbot
pnpm install
Bana sorulduğunda Next.js, tRPC ve PostgreSQL'i seçtim. Şimdilik Auth (Kimlik Doğrulama) kısmını atladım çünkü tekrar ediyorum; mükemmelleştirmeye değil, canlıya almaya odaklanıyoruz. NextAuth'u daha sonra on dakika içinde ekleyebilirsiniz.
Elde ettiğiniz monorepo yapısı:
my-chatbot/
├── apps/nextjs/ # Web uygulamanız
├── packages/
│ ├── api/ # tRPC router'ları (paylaşılan mantık)
│ ├── db/ # Veritabanı şeması + Drizzle
│ └── ui/ # Paylaşılan bileşenler
Bu ayrım, API mantığınızın web, mobil ve hatta CLI uygulamaları arasında yeniden kullanılabileceği anlamına gelir. Sırf her şeye tek bir klasörde başladıkları için aylarca refactoring (kod iyileştirme) yapmak zorunda kalan takımlar gördüm.
Adım 2: Beyin (Gemini)
OpenAI harika, ama Gemini Flash'ı denediniz mi? İnanılmaz derecede hızlı ve fiyatlandırması çok agresif. Gecikmenin (latency) tüm havayı bozduğu bir sohbet arayüzü için hız, bir özelliktir.
Neden GPT-3.5/4 yerine Gemini Flash?
- Hız: ~800ms'ye karşı 2-3sn yanıt süresi
- Maliyet: GPT-4'ten 60 kat daha ucuz
- Bağlam (Context): 1M token bağlam penceresi (evet, bir milyon)
LLM'lerle konuşmayı standart hale getirmek için AI SDK'sına ihtiyacımız var.
cd packages/api
pnpm add ai @ai-sdk/google
.env dosyanızı proje kök dizininde ayarlayın. Yerelde veritabanı konusunu fazla düşünmeyin; yerel bir Postgres örneği işinizi görecektir.
POSTGRES_URL="postgresql://user:pass@localhost:5432/chatbot"
GOOGLE_GENERATIVE_AI_API_KEY="your_key_here"
Profesyonel ipucu: Gemini API anahtarınızı https://aistudio.google.com/app/apikey adresinden alın. Ücretsiz katman saçma derecede cömert—dakikada 60 istek. Hız sınırlarına (rate limits) takılmadan önce Ürün-Pazar Uyumunu (PMF) yakalamış olursunuz.
Adım 3: Gerçekliği Tanımla (Şema)
İşte Drizzle'ın parladığı yer burası. Eskiden migrasyonları elle yazardınız. Şimdi şemanızı TypeScript ile tanımlıyorsunuz ve veritabanı itaat ediyor.
packages/db/src/schema.ts içinde bir "Message"ın ne olduğunu tanımlıyoruz. drizzle-zod kullandığımıza dikkat ettiniz mi? Bu, API'miz için doğrulama şemalarını otomatik olarak oluşturur. İşte "Kendini Tekrar Etme" (DRY) prensibinin eyleme geçmiş hali.
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 });
Gönderin gitsin: pnpm db:push. Bitti. Veritabanınız artık var.
Az önce ne oldu? Drizzle, TypeScript tanımınıza baktı ve tabloyu oluşturdu. SQL yazılmadı. Migrasyon dosyası yok. Şema odaklı geliştirmenin büyüsü budur.
Doğrulamak isterseniz: pnpm db:studio komutunu çalıştırın; https://local.drizzle.studio adresinde message tablonuzun veri almaya hazır bir şekilde beklediği web arayüzünü göreceksiniz.
Adım 4: Sinir Sistemi (tRPC)
İnsanların aklını başından alan kısım genellikle burasıdır. REST veya GraphQL ile uç noktaları (endpoints), tipleri ve fetcher'ları ayrı ayrı tanımlamanız gerekir. tRPC ile backend fonksiyonunuz aynı zamanda frontend fonksiyonunuzdur.
Kullanıcının mesajını kaydeden, geçmişi alan (AI'da bağlam kraldır), Gemini'ye gönderen ve yanıtı kaydeden bir prosedür oluşturuyoruz.
packages/api/src/router/chat.ts dosyasını oluşturun:
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;
Router'ı kaydedin 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;
Şu akışa bir bakın. Doğrusal, okunabilir ve tamamen tip güvenli. Veritabanı şemasını değiştirirseniz, bu kod anında kırmızıya döner. Çalışma zamanı (runtime) sürprizleri yok.
Neden .reverse()? Mesajları azalan sırada (en yeniden en eskiye) sorguluyoruz ama LLM'ler kronolojik sıra (en eskiden en yeniye) bekler. Bu, kafa karıştırıcı konuşmaları önleyen küçük bir detaydır.

Adım 5: Arayüz
apps/nextjs/src/app/chat/page.tsx içinde bağlantıyı kuruyoruz. tRPC kullandığımız için React Query bedavaya geliyor. useQuery veri çekme, önbellekleme ve yükleme durumlarını hallediyor; veri çekmek için tek bir useEffect bile yazmamıza gerek kalmıyor.
(Sadece en alta kaydırma işlemi için bir useEffect ekledim—çünkü UX önemlidir).
"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>
);
}
Ana sayfayı unutmayın. apps/nextjs/src/app/page.tsx dosyasını güncelleyin:
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>
);
}
pnpm dev komutunu çalıştırın ve http://localhost:3000 adresini ziyaret edin. "Start Chatting"e tıklayın ve işte çalışan bir AI chatbot'unuz var.
tRPC'nin sihri: Hiçbir API fetch işlemi yazmadığımızı fark ettiniz mi? fetch() çağrıları yok, URL stringleri yok, manuel hata yönetimi yok. TypeScript, sendMsg.mutate() fonksiyonunun ne beklediğini biliyor. Eğer backend giriş şemasını değiştirirseniz, frontend'iniz derleme hatası verecektir. Gelecek bu.
Adım 6: Ruh Katmak (Vibe Kontrolü)
Sıradan bir asistan sıkıcıdır. Sıradan bir asistan silinmeye mahkumdur. LLM'lerin güzelliği, mükemmel rol yapabilmeleridir.
Botunuza güçlü bir fikir/görüş vermenin onu 10 kat daha ilgi çekici hale getirdiğini keşfettim. Sadece "Yardımcı ol" diye komut vermeyin. Bir kişilik için komut verin.
Hadi backend'i bir persona kabul edecek şekilde değiştirelim. packages/api/src/router/chat.ts dosyasını güncelleyin:
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
};
Karakter seçimini iletmek için frontend'i güncelleyin:
// 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>
Artık sadece bir chatbot yapmadınız; bir karakter etkileşim platformu inşa ettiniz. İşte ürün budur.
Asıl Merak Ettiğiniz Teknik Detaylar
Neden sadece Prisma kullanmıyoruz?
Prisma harika, ama Drizzle daha hızlı. 2-3 kat sorgu performansından bahsediyoruz. Tek kişilik bir kurucu olduğunuzda, her milisaniye birikir. Ayrıca, Drizzle'ın SQL benzeri sözdizimi daha az zihinsel yük (mental overhead) anlamına gelir.
Peki ya akış (streaming) yanıtları?
Vercel AI SDK, kutudan çıktığı gibi streaming'i destekler. generateText yerine streamText kullanın ve frontend'de useChat hook'unu kullanın. Ben burada atladım çünkü bir eğitim yazısı için istek/yanıt (request/response) yapısı daha basit. Ama prodüksiyonda? Her şeyi stream edin. Toplam süre aynı olsa bile kullanıcılar streaming'i "daha hızlı" algılar.
Bağlam penceresi (context window) yönetimi
Şu anda son 10 mesajı alıyoruz. Bu, çalışmayana kadar çalışır. Ciddi bir ürün geliştiriyorsanız, bir token sayacı uygulayın ve geçmişi dinamik olarak ayarlayın. AI SDK'sında bunun için araçlar mevcut.
import { anthropic } from "@ai-sdk/anthropic";
const { text } = await generateText({
model: anthropic("claude-3-5-sonnet-20241022"),
maxTokens: 1000, // Control costs
// ...
});
Veritabanı bağlantı havuzu (connection pooling)
Yerel Postgres geliştirme için iyidir. Prodüksiyon için Vercel Postgres veya Supabase kullanın. Bağlantı havuzunu otomatik olarak hallederler. Serverless + veritabanı bağlantıları bir tuzaktır—bunu kendiniz yönetmeyin.
Pratik Çıkarımlar
Bunu okuyor ve kodlama isteği duyuyorsanız, işte tavsiyem:
- Sıfırdan başlamayın. Şablon kod (boilerplate) ivmenin düşmanıdır. T3 Turbo veya benzeri iskelet yapıları kullanın.
- Tip güvenliği hız demektir. İlk saatte daha yavaş hissettirir, sonraki on yıl boyunca daha hızlıdır. Genellikle demo sırasında ortaya çıkan hataları yakalar.
- Bağlam anahtardır. Geçmişi olmayan bir chatbot, sadece süslü bir arama çubuğudur. LLM'e her zaman son birkaç mesajı gönderin.
- Kişilik > özellikler. Tony Stark gibi konuşan bir bot, 10 ekstra özelliği olan sıradan bir bottan daha fazla etkileşim alacaktır.
Dağınık Gerçeklik
Bunu inşa etmek tereyağından kıl çeker gibi olmadı. Başlangıçta veritabanı bağlantı dizesini (connection string) yanlış yazdım ve Drizzle'ın bana neden bağırdığını anlamak için 20 dakika harcadım. Ayrıca başlangıçta çok fazla geçmiş gönderdiğim için Gemini'de hız sınırına takıldım (ders: her zaman .limit(5) ile başlayın ve sonra artırın).
Yükleme animasyonu mu? Onu doğru yapmak üç denememi aldı çünkü CSS animasyonları 2024'te bile hala bir şekilde kara büyü.
Ama olay şu: Sağlam bir stack kullandığım için bunlar yapısal problemler değil, mantıksal problemlerdi. Temel sağlam kaldı. Yanlış soyutlamayı seçtiğim için tüm API'yi refactor etmek zorunda kalmadım.
Canlıya Alın (Ship It)
İnşa etmenin altın çağında yaşıyoruz. Araçlar güçlü, yapay zeka zeki ve giriş bariyeri hiç bu kadar düşük olmamıştı.
Kod elinizde. Stack elinizde. Takasları (tradeoffs) anlıyorsunuz.
Gidin var olmaması gereken bir şey inşa edin ve akşam yemeğinden önce canlıya alın.
Toplam yapım süresi: ~2 saat Yazılan gerçek kod satırı: ~200 Prodüksiyonda karşılaşılan hatalar: 0 (şimdilik)
T3 stack + Gemini sadece hızlı değil—en iyi anlamda sıkıcı. Sürpriz yok. "Benim makinemde çalışıyor" sorunu yok. Sadece inşa etmek var.
Keyifli kodlamalar.
Kaynaklar:
Tam kod: github.com/giftedunicorn/my-chatbot
Bunu paylaş

Feng Liu
shenjian8628@gmail.com