How I Built an AI Trip Planner That Plans Trips in Seconds
Planning a trip sounds fun until you actually start doing it.
You open multiple tabs, compare hotels, check weather, convert currencies, and try to make all the pieces fit. What should be a small planning task turns into hours of research.
That friction is what led me to build AI Trip Planner.
The idea was simple: give the app a few trip details, and it should turn them into a real itinerary instead of a pile of random suggestions.
The Idea
I did not want another travel site that only shows information.
I wanted something that could actually help decide:
- where to go
- what to do each day
- how to balance the budget
- how to keep the plan realistic
So the app asks for a destination, number of days, budget, and travel preferences, then generates a structured day-by-day plan.
The result feels less like a search engine and more like a travel assistant.
How I Built It
The stack is intentionally practical:
- Frontend: Next.js 15 (App Router) with React 19, Tailwind CSS 4, Radix UI primitives for accessible components, and Framer Motion for the landing/planner transitions
- Backend: FastAPI on Python 3.10+, served by Uvicorn
- Agent layer: LangGraph, with LangChain/
langchain-groqas the default LLM provider (meta-llama/llama-4-scout-17b-16e-instruct) and an OpenAI fallback viaMODEL_PROVIDER=openai - Database: SQLModel (Pydantic + SQLAlchemy combined) with SQLite by default, PostgreSQL when
DATABASE_URLis set — the driver auto-upgrades frompostgresql://topostgresql+psycopg://
The frontend sends trip details to the backend through Next.js rewrites — /api/*, /query, and /health all forward to FastAPI — so the browser never needs to know the backend’s actual URL, and it sidesteps CORS friction in development.
On the backend, the request is converted into a natural-language prompt, wrapped into a TripDetails object, and passed into a compiled LangGraph agent living in app.state.react_app. It’s a ReAct-style graph: START → agent node, where the agent prepends a system prompt and calls the LLM with tools bound to it; if the LLM decides it needs more context, tools_condition routes to a ToolNode, which executes the relevant tool and loops back to the agent. Once there are no more tool calls, it routes to END. The tools available are:
- weather lookup (OpenWeatherMap — current conditions plus a 10-entry forecast)
- currency conversion (ExchangeRate-API)
- place search for attractions, restaurants, and activities (Tavily web search)
- budget and expense estimation (a calculator tool for hotel costs, total expense, and daily budget)
- mock transport discovery (flights, trains, buses)
That tool loop is what makes the output feel grounded. Instead of a generic paragraph, the model can reason step by step and shape the trip around real weather, real exchange rates, and real search results.
What the User Sees
From the outside, the flow is simple.
The user enters a few trip details, clicks generate, and gets back a trip plan that is already organized into days. Behind the scenes, FastAPI validates the request into an ItineraryGenerateRequest, runs the agent, extracts the final message, measures processing time, and kicks off a background asyncio.create_task to persist the trip to the database — so the response comes back to the user without waiting on the DB write.
I also kept the app stateful where it matters and lightweight where it does not. Trips and their itineraries are stored in two SQLModel tables — trip (question, answer, metadata, processing time, timestamp) and itinerary (a JSON-serialized day array, one-to-one with a trip) — so they can be viewed, updated, or deleted later. The request/response flow itself stays straightforward, and endpoints like /api/trips, /api/trips/{id}, /api/itinerary/generate, and /api/transport-options cover the CRUD and generation surface.
Why This Design Worked
The biggest decision was separating the utility code from the agent code.
The actual service clients live in utils/ — thin wrappers around OpenWeatherMap, Tavily, ExchangeRate-API, and the mock transport generator. The LangChain tool wrappers live in tools/, where each one wraps a utility client with an @tool decorator and defines the schema the LLM actually sees. That separation meant I could reuse the utility clients elsewhere — tests, a future CLI — without dragging LangChain along, and it kept the agent layer focused purely on orchestration.
A few other patterns fell out of that same instinct toward separation: GraphBuilder acts as a builder that constructs and compiles the LangGraph once at startup; ModelLoader is a factory-ish loader that picks Groq or OpenAI based on config; app/database.py acts as a lightweight repository wrapping the DB CRUD operations; and the Next.js rewrites act as a proxy layer between frontend and backend.
I also kept the transport system mocked on purpose. TransportDiscoveryTool generates flight, train, and bus options using a hard-coded city database, real Haversine-formula distance calculations, and a scoring heuristic weighted by cost (40%), duration (35%), and convenience (25%). It’s honestly presented as mock data — real APIs like Amadeus or Google Flights need paid keys and OAuth — but the interface is built so a real client could slot in behind the same tool without touching the endpoint or response model.
That tradeoff kept the project shippable while still letting the architecture feel complete.
What I Learned
This project taught me that useful AI products are mostly about systems, not just prompts.
I learned how to:
- connect a frontend and backend cleanly through rewrites instead of hardcoded API URLs
- use an agent only where multi-step reasoning actually helps, rather than reaching for LangGraph everywhere
- keep outputs structured and readable by parsing itinerary data into day-by-day JSON instead of trusting raw LLM markdown
- separate reusable services (
utils/) from AI-specific wrappers (tools/) - design for persistence and failure, not just the happy path — background writes, 503s when the agent hasn’t initialized, and a
model_decommissionedstring check that gracefully surfaces provider outages
It also reminded me that the best AI demo is the one that solves a real annoyance — and that being honest about the gaps (no auth on trip endpoints, no rate limiting on the LLM route, SQLite’s single-writer limits, mocked transport data) is part of actually understanding what I built, not just what it looks like from the outside.
What I Would Add Next
If I keep extending this project, the next steps are clear:
- real transport integrations behind the existing
TransportDiscoveryToolinterface - basic auth so trip endpoints aren’t accessible by anyone who can guess a sequential trip ID
- rate limiting on
/api/query, since every call triggers a real LLM invocation - map-based itinerary views (the frontend already ships
react-map-gl/Mapbox for this) - stronger budget validation — right now the LLM can still drift from the stated budget without a hard validation layer
- saved user preferences and more personalization over time
- moving off SQLite’s
check_same_thread=Falseworkaround toward PostgreSQL by default, now that background persistence runs through a thread pool
Final Thought
AI Trip Planner started as a way to make trip planning less painful.
It became one of my favorite projects because it combines practicality, orchestration, and a bit of AI reasoning in a way that feels genuinely useful.
Instead of spending hours planning a trip, you can get a structured plan in seconds.