Teil 1: So baust du einen Chatbot mit T3 Turbo & Gemini
Viele Gründer bleiben in der „Setup-Hölle“ stecken. Ich habe gerade an einem einzigen Nachmittag einen komplett typsicheren AI-Chatbot gebaut. Hier ist der genaue Stack – Next.js, tRPC und Gemini – und der Code zum Nachbauen.

Title: Wie du in 2 Stunden einen Chatbot baust: T3 Stack + Gemini AI
Content: Komplexität ist der stille Killer von Startups in der Frühphase. Du startest mit einer simplen Idee – „Ich will einen Chatbot, der wie Tony Stark spricht“ – und drei Wochen später konfigurierst du immer noch Webpack, kämpfst mit Docker-Containern oder debuggst einen Authentifizierungs-Flow, den noch kein einziger Nutzer verwendet hat.
Es ist eine Falle, in die ich brillant talentierte Ingenieure immer wieder tappen sehe. Wir lieben unsere Tools. Wir lieben es, zu optimieren. Aber im Startup-Game ist Shipping die einzige Metrik, die zählt.
Wenn du dir das moderne TypeScript-Ökosystem in letzter Zeit nicht angesehen hast, wirst du vielleicht überrascht sein. Die Zeiten, in denen man disparate APIs zusammenflicken musste und betete, dass sie halten, sind größtenteils vorbei. Wir sind in die Ära des „Vibe Coders“ eingetreten – wo die Distanz zwischen einer Idee und einem deployten Produkt in Stunden gemessen wird, nicht in Sprints.
Heute führe ich dich durch einen Tech-Stack, der sich wie ein Cheatcode anfühlt: Create T3 Turbo kombiniert mit Google's Gemini AI. Er ist typsicher von der Datenbank bis zum Frontend, unfassbar schnell und bringt ehrlich gesagt die Freude am Coden zurück.
Warum dieser Stack wichtig ist
Du denkst vielleicht: „Feng Liu, warum noch ein Stack? Kann ich nicht einfach Python und Streamlit nehmen?“
Klar, für einen Prototypen. Aber wenn du ein Produkt baust – etwas, das skalieren muss, Nutzer verwalten und State beibehalten soll – brauchst du eine echte Architektur. Das Problem ist, dass „echte Architektur“ normalerweise „Wochen voller Boilerplate-Code“ bedeutet.
Der T3 Stack (Next.js, tRPC, Tailwind) dreht dieses Skript um. Er gibt dir die Robustheit einer Full-Stack-Anwendung mit der Entwicklungsgeschwindigkeit eines Skripts. Wenn du Drizzle ORM (leichtgewichtig, SQL-ähnlich) und Google Gemini (schnell, großzügiger Free Tier) hinzufügst, hast du ein Toolkit, mit dem ein Solo-Gründer ein zehnköpfiges Team ausmanövrieren kann.
Lass uns etwas Echtes bauen.
Schritt 1: Das Ein-Befehl-Setup
Vergiss das manuelle Konfigurieren von ESLint und Prettier. Wir nutzen create-t3-turbo. Das setzt eine Monorepo-Struktur auf, was perfekt ist, weil es deine API-Logik von deinem Next.js-Frontend trennt. Damit bist du zukunftssicher, wenn du später unvermeidlich eine React Native Mobile App launchst.
pnpm create t3-turbo@latest my-chatbot
cd my-chatbot
pnpm install
Als ich gefragt wurde, habe ich Next.js, tRPC und PostgreSQL ausgewählt. Ich habe Auth erst einmal weggelassen, denn nochmal: Wir optimieren auf Shipping, nicht auf Perfektion. NextAuth kannst du später in zehn Minuten hinzufügen.
Die Monorepo-Struktur, die du bekommst:
my-chatbot/
├── apps/nextjs/ # Deine Web App
├── packages/
│ ├── api/ # tRPC Router (geteilte Logik)
│ ├── db/ # Datenbankschema + Drizzle
│ └── ui/ # Geteilte Komponenten
Diese Trennung bedeutet, dass deine API-Logik über Web, Mobile oder sogar CLI-Apps hinweg wiederverwendet werden kann. Ich habe Teams gesehen, die Monate mit Refactoring verschwendet haben, weil sie alles in einem Ordner begonnen hatten.
Schritt 2: Das Gehirn (Gemini)
OpenAI ist super, aber hast du schon Gemini Flash ausprobiert? Es ist unglaublich schnell und das Pricing ist aggressiv. Für ein Chat-Interface, wo Latenz den Vibe killt, ist Geschwindigkeit ein Feature.
Warum Gemini Flash statt GPT-3.5/4?
- Geschwindigkeit: ~800ms vs. 2-3s Antwortzeit
- Kosten: 60x günstiger als GPT-4
- Kontext: 1M Token Kontext-Fenster (ja, eine Million)
Wir brauchen das AI SDK, um die Kommunikation mit LLMs zu standardisieren.
cd packages/api
pnpm add ai @ai-sdk/google
Richte deine .env im Project Root ein. Zerdenk die Datenbank lokal nicht; eine lokale Postgres-Instanz reicht völlig.
POSTGRES_URL="postgresql://user:pass@localhost:5432/chatbot"
GOOGLE_GENERATIVE_AI_API_KEY="your_key_here"
Pro-Tipp: Hol dir deinen Gemini API Key von https://aistudio.google.com/app/apikey. Der Free Tier ist absurd großzügig – 60 Anfragen pro Minute. Du wirst Product-Market-Fit erreichen, bevor du in die Rate Limits läufst.
Schritt 3: Die Realität definieren (Das Schema)
Hier glänzt Drizzle. Früher hast du Migrationen von Hand geschrieben. Jetzt definierst du dein Schema in TypeScript und die Datenbank gehorcht.
In packages/db/src/schema.ts definieren wir, was eine „Message“ ist. Siehst du, wie wir drizzle-zod verwenden? Das erstellt automatisch Validierungs-Schemas für unsere API. Das ist das „Don't Repeat Yourself“-Prinzip in Aktion.
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 es: pnpm db:push. Fertig. Deine Datenbank existiert jetzt.
Was ist gerade passiert? Drizzle hat sich deine TypeScript-Definition angesehen und die Tabelle erstellt. Kein SQL geschrieben. Keine Migrationsdateien. Das ist die Magie von Schema-getriebener Entwicklung.
Wenn du es überprüfen willst, führe pnpm db:studio aus und du siehst eine Web-UI unter https://local.drizzle.studio, wo deine message-Tabelle darauf wartet, Daten zu empfangen.
Schritt 4: Das Nervensystem (tRPC)
Das ist der Teil, der die Leute meistens umhaut. Mit REST oder GraphQL musst du Endpunkte, Typen und Fetcher separat definieren. Mit tRPC ist deine Backend-Funktion deine Frontend-Funktion.
Wir erstellen eine Prozedur, die die Nachricht des Nutzers speichert, den Verlauf abruft (Kontext ist King bei KI), sie an Gemini sendet und die Antwort speichert.
Erstelle 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;
Registriere den Router in 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;
Schau dir diesen Flow an. Er ist linear, lesbar und vollständig typisiert. Wenn du das Datenbankschema änderst, wird dieser Code sofort rot markiert. Keine Überraschungen zur Laufzeit.
Warum das .reverse()? Wir fragen Nachrichten in absteigender Reihenfolge ab (neueste zuerst), aber LLMs erwarten chronologische Reihenfolge (älteste zuerst). Es ist ein winziges Detail, das verwirrende Konversationen verhindert.

Schritt 5: Das Interface
In apps/nextjs/src/app/chat/page.tsx verbinden wir alles. Weil wir tRPC nutzen, bekommen wir React Query gratis dazu. useQuery kümmert sich um das Fetching, Caching und die Ladezustände, ohne dass wir ein einziges useEffect für das Datenladen schreiben müssen.
(Ich habe ein useEffect nur für das Scrollen nach unten eingefügt – weil UX zählt).
"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>
);
}
Vergiss die Homepage nicht. Aktualisiere 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>
);
}
Führe pnpm dev aus und besuche http://localhost:3000. Klicke auf „Start Chatting“ und du hast einen funktionierenden AI-Chatbot.
Die Magie von tRPC: Ist dir aufgefallen, dass wir nie einen API-Fetch geschrieben haben? Keine fetch()-Aufrufe, keine URL-Strings, kein manuelles Error-Handling. TypeScript weiß, was sendMsg.mutate() erwartet. Wenn du das Backend-Input-Schema änderst, wirft dein Frontend einen Compile-Fehler. Das ist die Zukunft.
Schritt 6: Seele einhauchen (Der „Vibe“-Check)
Ein generischer Assistent ist langweilig. Ein generischer Assistent wird gelöscht. Das Schöne an LLMs ist, dass sie exzellente Rollenspieler sind.
Ich habe festgestellt, dass es deinen Bot 10x fesselnder macht, wenn du ihm eine starke Meinung gibst. Prompte nicht einfach „Du bist hilfreich.“ Prompte für eine Persönlichkeit.
Lass uns das Backend so anpassen, dass es eine Persona akzeptiert. 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
};
Aktualisiere das Frontend, um die Charakterauswahl zu übergeben:
// 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>
Jetzt hast du nicht nur einen Chatbot gebaut; du hast eine Plattform für Charakter-Interaktion gebaut. Das ist ein Produkt.
Die technischen Details, die dich wirklich interessieren
Warum nicht einfach Prisma nutzen?
Prisma ist toll, aber Drizzle ist schneller. Wir sprechen von 2-3x Query-Performance. Wenn du ein Solo-Gründer bist, summiert sich jede Millisekunde. Plus: Die SQL-ähnliche Syntax von Drizzle bedeutet weniger mentalen Overhead.
Was ist mit Streaming Responses?
Das Vercel AI SDK unterstützt Streaming out-of-the-box. Ersetze generateText durch streamText und nutze den useChat-Hook im Frontend. Ich habe es hier übersprungen, weil Request/Response für ein Tutorial einfacher ist. Aber in Production? Streame alles. Nutzer nehmen Streaming als „schneller“ wahr, selbst wenn die Gesamtzeit gleich ist.
Kontext-Fenster-Management
Im Moment greifen wir uns die letzten 10 Nachrichten. Das funktioniert, bis es nicht mehr funktioniert. Wenn du ein ernsthaftes Produkt baust, implementiere einen Token-Counter und passe den Verlauf dynamisch an. Das AI SDK hat Utilities dafür.
import { anthropic } from "@ai-sdk/anthropic";
const { text } = await generateText({
model: anthropic("claude-3-5-sonnet-20241022"),
maxTokens: 1000, // Control costs
// ...
});
Datenbank Connection Pooling
Lokal ist Postgres völlig okay für Dev. Für Production nutze Vercel Postgres oder Supabase. Die kümmern sich automatisch um Connection Pooling. Serverless + Datenbankverbindungen ist eine Falle – verwalte das nicht selbst.
Praktische Takeaways
Wenn du das hier liest und es dir in den Fingern juckt zu coden, hier ist mein Rat:
- Fang nicht bei Null an. Boilerplate ist der Feind des Momentums. Nutze T3 Turbo oder ein ähnliches Gerüst.
- Typsicherheit ist Geschwindigkeit. Es fühlt sich in der ersten Stunde langsamer an, und für die nächsten zehn Jahre schneller. Sie fängt die Bugs ab, die normalerweise während einer Demo passieren.
- Kontext ist der Schlüssel. Ein Chatbot ohne Verlauf ist nur eine schicke Suchleiste. Übergib immer die letzten paar Nachrichten an das LLM.
- Persönlichkeit > Features. Ein Bot, der wie Tony Stark klingt, wird mehr Engagement bekommen als ein generischer Bot mit 10 Extra-Features.
Die chaotische Realität
Das hier zu bauen war nicht nur „alles glatt gelaufen“. Ich habe anfangs den Datenbank-Connection-String vermasselt und 20 Minuten damit verbracht, mich zu fragen, warum Drizzle mich anschreit. Ich bin auch in ein Rate Limit bei Gemini gelaufen, weil ich anfangs zu viel Verlauf gesendet habe (Lektion: starte immer mit .limit(5) und skaliere dann hoch).
Die Ladeanimation? Die hat mich drei Versuche gekostet, bis sie passte, weil CSS-Animationen auch 2024 irgendwie immer noch schwarze Magie sind.
Aber hier ist der Punkt: Weil ich einen robusten Stack genutzt habe, waren das logische Probleme, keine strukturellen Probleme. Das Fundament hielt stand. Ich musste nie die gesamte API refactorn, nur weil ich die falsche Abstraktion gewählt hatte.
Ship It
Wir leben in einem goldenen Zeitalter des Bauens. Die Tools sind mächtig, die KI ist schlau und die Einstiegshürde war noch nie niedriger.
Du hast jetzt den Code. Du hast den Stack. Du verstehst die Trade-offs.
Geh raus, bau etwas, das eigentlich nicht existieren sollte, und shippe es noch vor dem Abendessen.
Gesamte Bauzeit: ~2 Stunden Zeilen an echtem Code: ~200 Bugs in Production: 0 (bis jetzt)
Der T3 Stack + Gemini ist nicht nur schnell – er ist langweilig auf die bestmögliche Art. Keine Überraschungen. Kein „Auf meiner Maschine läuft's aber.“ Einfach nur Bauen.
Happy Coding.
Ressourcen:
Vollständiger Code: github.com/giftedunicorn/my-chatbot
Excerpt: Komplexität tötet Startups. In diesem Guide zeige ich dir, wie du mit dem T3 Stack und Google Gemini in Rekordzeit einen voll typisierten, charakterstarken Chatbot baust. Kein Boilerplate-Albtraum, nur reines Shipping.
Teilen

Feng Liu
shenjian8628@gmail.com