DEV Community

Cover image for I Built a Travel Assistant with Gemini 2.5 Flash: Here's How
Developer on Travel
Developer on Travel

Posted on

I Built a Travel Assistant with Gemini 2.5 Flash: Here's How

Google I/O Writing Challenge Submission

This is a submission for the Google I/O Writing Challenge

Google I/O 2026 dropped a lot of announcements, but the one that made me immediately open my code editor was Gemini 2.5 Flash. Faster, smarter, and free to start — no credit card needed.

So I built something real with it: a conversational travel assistant that answers questions about hotels, visas, itineraries, and local tips. In this post I'll walk you through every step so you can build it too.


What is Gemini 2.5 Flash?

Announced at Google I/O 2026, Gemini 2.5 Flash is Google's fastest model in the 2.5 family — optimized for low latency while still being genuinely capable. It supports:

  • 1M token context window — enough for very long conversations
  • System instructions — give the model a persistent role and persona
  • Multi-turn chat — native conversation history support
  • 1,500 free requests/day via Google AI Studio — no credit card required

What We're Building

A Next.js app with:

  • A Next.js Route Handler as a secure proxy to the Gemini API (your API key never reaches the browser)
  • A React chat UI with suggestion chips, conversation history, typing indicator, and proper markdown rendering

Here's the project structure:

gemini-travel-assistant/
├── app/
│   ├── api/chat/route.ts   ← Gemini API proxy
│   ├── page.tsx            ← main page
│   └── layout.tsx
├── components/
│   └── ChatWidget.tsx      ← chat UI
└── .env.local              ← your API key
Enter fullscreen mode Exit fullscreen mode

Step 1: Create the Project

npx create-next-app@latest gemini-travel-assistant \
  --typescript --tailwind --app --yes

cd gemini-travel-assistant
npm install @google/generative-ai react-markdown
Enter fullscreen mode Exit fullscreen mode

Step 2: Get Your Free API Key

  1. Go to aistudio.google.com
  2. Sign in with Google → click Get API Key
  3. Create .env.local in your project root:
GEMINI_API_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

No billing setup needed.


Step 3: The API Route

Create app/api/chat/route.ts:

import { GoogleGenerativeAI } from "@google/generative-ai";
import { NextRequest, NextResponse } from "next/server";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

const SYSTEM_PROMPT = `You are a friendly and knowledgeable travel assistant powered by Gemini 2.5 Flash.
You help travelers with:
- Hotel and accommodation recommendations
- Travel tips and itineraries
- Visa and entry requirements
- Budget planning
- Local culture, food, and activities
- Best times to visit destinations

Keep your answers concise, practical, and helpful. Use bullet points when listing multiple items.
If you don't know something, say so honestly.`;

export async function POST(req: NextRequest) {
  try {
    const { message, history } = await req.json();

    if (!message) {
      return NextResponse.json({ error: "Message is required" }, { status: 400 });
    }

    const model = genAI.getGenerativeModel({
      model: "gemini-2.5-flash",
      systemInstruction: SYSTEM_PROMPT,
    });

    const chat = model.startChat({
      history: history || [],
    });

    const result = await chat.sendMessage(message);
    const text = result.response.text();

    return NextResponse.json({ reply: text });
  } catch (error) {
    console.error("Gemini API error:", error);
    return NextResponse.json(
      { error: "Failed to get response from Gemini" },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Two things to notice:

  • systemInstruction gives Gemini a persistent role across the entire conversation without repeating it every turn
  • history is sent by the client on every request — the Gemini API is stateless, so you own the conversation history

Step 4: The Chat Component

Create components/ChatWidget.tsx:

"use client";

import { useState, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";

type Role = "user" | "model";

interface Message {
  role: Role;
  text: string;
}

const SUGGESTIONS = [
  "Best hotels in Tokyo under $100?",
  "Visa requirements for Pakistan to Japan",
  "7-day itinerary for Paris",
  "Best time to visit Bali",
];

export default function ChatWidget() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const bottomRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, loading]);

  async function sendMessage(text: string) {
    if (!text.trim() || loading) return;

    const userMessage: Message = { role: "user", text };
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput("");
    setLoading(true);

    try {
      const history = messages.map((m) => ({
        role: m.role,
        parts: [{ text: m.text }],
      }));

      const res = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: text, history }),
      });

      const data = await res.json();
      if (data.error) throw new Error(data.error);

      setMessages([...updatedMessages, { role: "model", text: data.reply }]);
    } catch {
      setMessages([
        ...updatedMessages,
        { role: "model", text: "Sorry, something went wrong. Please try again." },
      ]);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.length === 0 && (
          <div className="flex flex-col items-center justify-center h-full text-center gap-6">
            <div>
              <div className="text-5xl mb-3">✈️</div>
              <h2 className="text-xl font-semibold text-gray-700">
                Ask me anything about travel
              </h2>
              <p className="text-gray-400 text-sm mt-1">
                Powered by Gemini 2.5 Flash
              </p>
            </div>
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full max-w-lg">
              {SUGGESTIONS.map((s) => (
                <button
                  key={s}
                  onClick={() => sendMessage(s)}
                  className="text-left text-sm bg-blue-50 hover:bg-blue-100 text-blue-700 rounded-xl px-4 py-3 transition-colors border border-blue-100"
                >
                  {s}
                </button>
              ))}
            </div>
          </div>
        )}

        {messages.map((msg, i) => (
          <div
            key={i}
            className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
          >
            {msg.role === "model" && (
              <div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-xs font-bold mr-2 shrink-0 mt-1">
                G
              </div>
            )}
            <div
              className={`max-w-[75%] rounded-2xl px-4 py-3 text-sm leading-relaxed ${
                msg.role === "user"
                  ? "bg-blue-600 text-white rounded-br-sm"
                  : "bg-gray-100 text-gray-800 rounded-bl-sm"
              }`}
            >
              {msg.role === "user" ? (
                msg.text
              ) : (
                <ReactMarkdown
                  components={{
                    p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
                    ul: ({ children }) => <ul className="list-disc pl-4 mb-2 space-y-1">{children}</ul>,
                    ol: ({ children }) => <ol className="list-decimal pl-4 mb-2 space-y-1">{children}</ol>,
                    strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
                    code: ({ children }) => <code className="bg-gray-200 rounded px-1 text-xs font-mono">{children}</code>,
                  }}
                >
                  {msg.text}
                </ReactMarkdown>
              )}
            </div>
          </div>
        ))}

        {loading && (
          <div className="flex justify-start">
            <div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-xs font-bold mr-2 shrink-0">
              G
            </div>
            <div className="bg-gray-100 rounded-2xl rounded-bl-sm px-4 py-3">
              <div className="flex gap-1 items-center h-4">
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0ms]" />
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:150ms]" />
                <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:300ms]" />
              </div>
            </div>
          </div>
        )}

        <div ref={bottomRef} />
      </div>

      <div className="border-t border-gray-200 p-4">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            sendMessage(input);
          }}
          className="flex gap-2"
        >
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Ask about hotels, visas, itineraries..."
            className="flex-1 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={loading || !input.trim()}
            className="bg-blue-600 hover:bg-blue-700 disabled:opacity-40 text-white rounded-xl px-5 py-3 text-sm font-medium transition-colors"
          >
            Send
          </button>
        </form>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The key pattern: on every request we rebuild the history array from local state and send it to the server. This is how Gemini maintains context — it's stateless on the API side, so you own the conversation history.


Step 5: The Main Page

Replace app/page.tsx:

import ChatWidget from "@/components/ChatWidget";

export default function Home() {
  return (
    <main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
      <div className="w-full max-w-2xl bg-white rounded-3xl shadow-2xl overflow-hidden flex flex-col h-[85vh]">
        <div className="bg-blue-600 px-6 py-4 flex items-center gap-3">
          <div className="text-2xl">✈️</div>
          <div>
            <h1 className="text-white font-bold text-lg leading-tight">
              Gemini Travel Assistant
            </h1>
            <p className="text-blue-200 text-xs">
              Powered by Gemini 2.5 Flash · Google I/O 2026
            </p>
          </div>
          <div className="ml-auto flex items-center gap-1.5">
            <span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
            <span className="text-blue-200 text-xs">Online</span>
          </div>
        </div>
        <div className="flex-1 overflow-hidden">
          <ChatWidget />
        </div>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Run It

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 — that's it. No Docker, no database, no paid tier.


My Take on Gemini 2.5 Flash

After building with it, here's what stood out:

What impressed me:

  • Speed — responses feel near-instant even with a full conversation history attached
  • System instructions are rock solid — the model stays in its travel assistant persona throughout, never drifting
  • The free tier is real — 1,500 requests/day is enough to build, demo, and show off a finished app
  • Markdown output — it naturally formats responses with bold text, bullet points, and headers without you asking

What to watch:

  • The API is stateless — you manage history yourself. It's clean architecturally but easy to miss on your first build
  • Double-check the model ID in the SDK docs as they evolve quickly — gemini-2.5-flash is current as of I/O 2026

Overall, Gemini 2.5 Flash is the most accessible entry point into production-quality AI I've used. Zero to a working app in under an hour, for free. That's what impressed me most at Google I/O 2026.


Full Source Code

GitHub logo mushahidmehdi / gemini-travel-assistant

Google I/O 2026 dropped a lot of announcements, but the one that made me immediately open my code editor was **Gemini 2.5 Flash**. Faster, smarter, and free to start — no credit card needed.

✈️ Gemini Travel Assistant

A conversational travel assistant built with Next.js and Gemini 2.5 Flash — answers questions about hotels, visas, itineraries, and local tips.

Built for the Google I/O 2026 Writing Challenge on DEV.


Features

  • 💬 Multi-turn conversation with full history
  • 🗺️ Answers about hotels, visas, itineraries, budgets, and more
  • ⚡ Powered by Gemini 2.5 Flash — fast and free to start
  • 🔒 API key stays server-side via Next.js Route Handler proxy
  • 📝 Markdown responses rendered cleanly in the UI

Tech Stack


























Tool Purpose
Next.js 16 App Router + API Route Handlers
Gemini 2.5 Flash AI via @google/generative-ai
Tailwind CSS Styling
react-markdown Render Gemini's markdown output


Getting Started

1. Clone the repo

git clone https://github.com/mushahidmehdi/gemini-travel-assistant.git
cd gemini-travel-assistant
Enter fullscreen mode Exit fullscreen mode

2. Install dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

3. Get a free Gemini API key

Go to aistudio.google.com, sign in with Google, and click Get API Key. No credit card required — 1,500…





Built for the Google I/O 2026 Writing Challenge on DEV.

Top comments (0)