Intro
As a software Engineer, there's immense satisfaction in transforming an idea into a functional application. My latest project, Tanaya, is a testament to this journey. It's a travel planning application built with Next.js, designed to simplify the process of finding and organizing travel recommendations.
This article will walk you through Trippers features, its technical foundation, and the key development challenge I navigated to bring it to life.
Your next adventure starts here. The main interface invites users to begin their journey with a few simple details. 🥰
The Vision Behind Tanaya
The idea for Tanaya stemmed from a common pain point: the often overwhelming process of planning a trip. Sifting through countless options, comparing prices, and managing itineraries can be tedious.
Tanaya aims to streamline this by providing a centralized platform where users can:
- Discover Recommendations: Input their budget, origin, travel dates and number of travelers to receive tailored trip suggestions.
- Filter and Sort: Refine recommendations by specific interests (tags) and sort them by price.
- Manage Trips: (Future-proofed) Simulate booking and saving trips for easy access.
- Personalized Experience: The Long-term vision includes integrating with advanced AI API (like Google’s Gemini API) to generate highly personalized and dynamic travel itineraries.
A clean and intuitive interface invites users to input their travel preferences, forming the foundation for their personalized trip recommendations.
Diving into the tech Stack
To build a modern, performant, and scalable web application, I chose a robust and widely adopted technology stack:
Next.js(React Framework): The backbone of Tripper. Next.js provides powerful features like server-side logic and external API integration.
React: For building the interactive and component-driven user interface. React’s declarative nature made it efficient to manage UI state and create reusable components.
Tailwind CSS: A utility-first CSS framework that allowed for rapid UI development and ensured a consistent design system. Its responsive utilities were invaluable for adapting the layout across different screen sizes.
Shadn/ui: A collection of beautifully designed, accessible, and customizable UI components built on top ofRadix UIandTailwind CSS. This significantly accelerated development by providing ready-to-use elements like Button, Input, Select, Card and DatePicker.
Next.js APIcalls (e.g., to a futureGeminiintegration) and for handling sensitive operations like simulated reservations and saved trips.
Key Development Challenges & Solutions
Building Tanaya presented several interesting challenges, each offering valuable learning opportunities:
1. Crafting a Responsive User Experience
Challenge: Ensuring the application looked and functioned flawlessly across various devices, from large desktop monitors to small mobile phones. Specific issues arose with filter controls and price sorting options, which initially didn’t adapt well to smaller screens, leading to layout overflows.
Solution: I leveraged Tailwind CSS’s responsive design principles. By strategically applying utility classes like flex-wrap, gap and responsive breakpoints (md:, lg:), I was able to create flexible layouts that automatically adjust. For instance, converting rigid space-x layouts to flex-wrap with gap allowed elements to flow onto new lines when horizontal space became limited, significantly improving the mobile experience.
2. Managing Asynchronous Data Flow and UI State Consistency
Challenge: In a dynamic application like Tanaya, fetching data (like travel recommendations) from an API is an asynchronous process. The challenge lies in effectively managing the different states of this data: loading, success and error. without careful handling, this can lead to a disjointed user experience, where the UI doesn’t accurately reflect the data’s current status or even to bugs like displaying stale data or failing silently. Ensuring the UI remains responsive and provides clear feedback during these operations is critical.
Solution: I tackled this by meticulously managing components state using
React’s useState and useEffect hooks.- Loading States: A loading state variable is set to true before an API call and false afterwards, allowing for visual indicators (like skeleton loaders) to inform the user.
- Data synchronization: The recommendations state is updated only upon successful data retrieval.
- Error Handling: try…catch blocks are used to gracefully handle API errors, preventing crashes and allowing for user-friendly error messages.
- URL-Driven Data Fetching: By linking the data fetching process to the URL’s query parameters (using
useSearchParamsanduseEffect), the applications ensures that the displayed recommendations are always synchronized with the user’s current search criteria, even after navigation or page refreshes. This pattern provides a robust and predictable data flow.
This approach ensures that the user always has a clear understanding of the application’s status and that the data displayed is accurate and up-to-date.
Frontend: Fetching Trip Recommendations
useEffect(() => { const fetchRecommendations = async () => { const budgetParam = getParam("budget"); const originParam = getParam("origin"); const fromParam = getParam("from"); const toParam = getParam("to"); const tagsParam = getParam("tags"); const travelersParam = getParam("travelers"); if (!originParam || !fromParam || !toParam) { setRecommendations([]); setLoading(false); return; } setLoading(true); try { const response = await fetch( `/api/recommendations?budget=${budgetParam || 0}&origin=${originParam}&from=${fromParam}&to=${toParam}&tags=${tagsParam || ''}&travelers=${travelersParam || 1}` ); const data = await response.json(); setRecommendations(data); } catch (error) { console.error("Failed to fetch recommendations:", error); setRecommendations([]); } finally { setLoading(false); } }; if (showRecommendations) { fetchRecommendations(); } }, [searchParams, showRecommendations, getParam]);
Frontend: Loading User-Specific Data on the Dashboard
useEffect(() => { if (status === "authenticated") { const fetchData = async () => { setLoading(true); try { // Fetch saved trips const savedTripsResponse = await fetch("/api/saved-trips"); const savedTripsData = await savedTripsResponse.json(); setSavedTrips(savedTripsData); // Fetch reservations const reservationsResponse = await fetch("/api/reservations"); const reservationsData = await reservationsResponse.json(); setReservations(reservationsData); } catch (error) { console.error("Error fetching dashboard data:", error); } finally { setLoading(false); } }; fetchData(); } else if (status === "unauthenticated") { setLoading(false); } }, [status]);
A users can saving the trips destinations.
A confirmation message shown after successfully booking a trip.
The user dashboard displaying a history of past reservations and saved trips for future reference.
3. Integrating with External APIs (and the Road to AI)
Challenge: Moving beyond static or mock data to integrate with real, external APIs presents a unique set of complexities. This involves
not just making HTTP requests, but also securely handling API keys, managing asynchronous operations, transforming data, and gracefully
handling network errors or API-specific responses. For Tripper, the ultimate goal is to integrate with powerful AI models like Google’s
Gemini API, which adds another layer of sophistication.
Solution: Next.js API Routes proved to be the ideal solution for this challenge. They act as a secure and efficient intermediary
between the client-side (browser) and external APIs.
- Security: Crucially, API keys for external services (like Gemini) are stored as environment variables on the server and are never exposed to the client. All calls to these external APIs are proxied through Tripper’s own API routes.
- Abstraction & Control: API Routes allow me to abstract away the complexities of external API calls. I can define custom endpoints (e.g., /api/recommendations) that handle the logic of constructing requests to the Gemini API, processing its responses, and then returning a simplified, application-specific data format to the frontend.
- Error Handling: Centralizing API calls in API Routes makes error handling more robust. I can catch network errors, handle API rate limits, and return user-friendly error messages to the client without exposing sensitive backend details.
- Future-Proofing: This architecture makes it straightforward to swap out or add new external APIs in the future. The frontend only needs to know about Tripper’s internal API routes, not the specifics of the external services they interact with.
This strategic use of API Routes is fundamental to Tripper’s ability to evolve from a mock-data application into a truly intelligent travel planner powered by real-time AI recommendations.
Backend: Recommendations API Route
import { NextResponse } from "next/server"; import { getRecommendations } from "@/lib/recommendations"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const budget = Number(searchParams.get("budget")) || 0; const origin = searchParams.get("origin") || ""; const from = searchParams.get("from"); const to = searchParams.get("to"); const tags = searchParams.get("tags"); const travelers = Number(searchParams.get("travelers")) || 1; if (!from || !to) { return NextResponse.json( { error: "Date range parameters are missing" }, { status: 400 } ); } const recommendations = await getRecommendations( budget, origin, { from: new Date(from), to: new Date(to), }, tags ? tags.split(",") : [], travelers ); return NextResponse.json(recommendations); }
4. Securing API Integrations (Beyond Just External APIs)
Challenge: Even my own Next.js API routes, which serve as the gateway to external services, needed protection. Without proper
safeguards, they could be directly accessed by unauthorized parties, potentially leading to abuse or unexpected costs from external API
usage.
Solution: To prevent unauthorized direct access to Tripper’s internal API routes, I implemented a custom API key mechanism. The
frontend sends a unique X-API-Key header with each request to my API routes. The API route then validates this key against a
server-side environment variable. This provides a crucial layer of defense against casual abuse and ensures that only legitimate
requests from the application are processed.
The sign-in page for accessing the user dashboard.
Backend: Protected API Route for Creating Reservations
import { NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { addMockReservation, Reservation } from "@/lib/mock-reservations"; // ... (GET function is here) ... export async function POST(request: Request) { const session = await getServerSession(); if (!session || !session.user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { const reservationData: Reservation = await request.json(); // Basic validation if (!reservationData.tripId || !reservationData.price) { return NextResponse.json( { error: "Missing required reservation data" }, { status: 400 } ); } // Generate a simple reservation ID reservationData.reservationId = `RES-${Date.now()}`; reservationData.status = "confirmed"; // Mocking immediate confirmation reservationData.date = new Date().toISOString(); reservationData.userId = session.user.id; const newReservation = addMockReservation(reservationData); return NextResponse.json(newReservation, { status: 201 }); } catch (error) { console.error("Error creating mock reservation:", error); return NextResponse.json( { error: "Failed to create reservation" }, { status: 500 } ); } }
Future Enhancements
Tanaya is a dynamic project with a clear roadmap for future development:
- Advanced AI Integration: Fully integrating the Gemini API to provide more intelligent, context-aware, and personalized travel recommendations. This will involve fine-tuning prompts, handling diverse AI responses, and potentially implementing more sophisticated data parsing.
- Robust Input Validation: Implementing comprehensive frontend and backend validation to ensure data integrity and enhance security.
- Enhanced User Feedback: Replacing basic alert() messages with a more sophisticated and non-intrusive notification system (e.g., toast messages).
- Full Authentication System: Implementing a proper user authentication system (e.g.,
NextAuth.js) to enable personalized user profiles, persistent saved trips, and secure booking functionalities.
- Optimized Data Fetching: Exploring libraries like SWR or React Query for advanced caching, revalidation, and streamlined data management.
Conclusion
Building Tanaya has been an incredibly enriching experience, allowing me to apply and deepen my understanding of modern web development principles. From crafting responsive UIs and managing complex application state to securely integrating with external services and planning for AI, each challenge has contributed to my growth as a software engineer. Tripper stands as a testament to the power of Next.js and the exciting possibilities of integrating AI into everyday applications.
👉 Follow me on twitter for more awesome stuff like this @mumeyong
