Next.js 14 App Router: Building Production-Ready Travel Platforms
February 1, 2025
10 min read
Next.js 14 App Router: Building Production-Ready Travel Platforms
When building Bisoa Travels, I had the opportunity to leverage Next.js 14's latest features. Here's what I learned building a production travel platform from scratch.
Why Next.js 14?
The App Router isn't just a new routing system - it's a fundamental shift in how we build React applications:
- Server Components by default - Better performance out of the box
- Streaming & Suspense - Progressive page loading
- Better SEO - True server-side rendering
- TypeScript-first - Better developer experience
Project Structure
bisoa-travels/
├── app/
│ ├── (routes)/
│ │ ├── destinations/
│ │ ├── bookings/
│ │ └── contact/
│ ├── api/
│ │ ├── bookings/route.ts
│ │ └── documents/route.ts
│ └── layout.tsx
├── components/
├── lib/
└── prisma/
Server Components FTW
One of the biggest wins: fetching data directly in components:
// app/destinations/[id]/page.tsx import { prisma } from '@/lib/prisma' export default async function DestinationPage({ params }: { params: { id: string } }) { // This runs on the server! const destination = await prisma.destination.findUnique({ where: { id: params.id }, include: { hotels: true, activities: true, reviews: true } }) return <DestinationDetails destination={destination} /> }
No useState, no useEffect, no loading states - just clean, simple code.
Streaming with Suspense
For slow-loading sections, use Suspense:
import { Suspense } from 'react' export default function BookingPage() { return ( <div> <Hero /> <Suspense fallback={<PackagesSkeleton />}> <TravelPackages /> </Suspense> <Suspense fallback={<ReviewsSkeleton />}> <CustomerReviews /> </Suspense> </div> ) }
The page loads progressively - users see content immediately!
API Routes with Route Handlers
Creating booking APIs is clean and type-safe:
// app/api/bookings/route.ts import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { prisma } from '@/lib/prisma' const BookingSchema = z.object({ destination: z.string(), startDate: z.string().datetime(), endDate: z.string().datetime(), travelers: z.number().min(1), email: z.string().email() }) export async function POST(request: NextRequest) { try { const body = await request.json() const data = BookingSchema.parse(body) const booking = await prisma.booking.create({ data: { ...data, status: 'PENDING' } }) // Send confirmation email await sendBookingConfirmation(booking) return NextResponse.json(booking, { status: 201 }) } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json( { error: 'Validation failed', details: error.errors }, { status: 400 } ) } return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } }
Image Optimization
Next.js Image component is amazing:
import Image from 'next/image' <Image src="/destinations/paris.jpg" alt="Paris destination" width={800} height={600} placeholder="blur" blurDataURL="data:image/..." priority={isAboveFold} />
Result: Automatic WebP/AVIF conversion, responsive sizes, lazy loading!
Document Verification with Google Cloud Vision
For travel document uploads, I integrated AI verification:
// app/api/documents/verify/route.ts import { ImageAnnotatorClient } from '@google-cloud/vision' const client = new ImageAnnotatorClient() export async function POST(request: NextRequest) { const formData = await request.formData() const file = formData.get('document') as File const buffer = Buffer.from(await file.arrayBuffer()) // Detect text in document const [result] = await client.textDetection(buffer) const text = result.fullTextAnnotation?.text // Validate passport/ID const isValid = validateDocument(text) return NextResponse.json({ valid: isValid, confidence: result.confidence }) }
Performance Optimizations
1. Metadata for SEO
// app/destinations/[id]/page.tsx export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> { const destination = await getDestination(params.id) return { title: `${destination.name} - Bisoa Travels`, description: destination.description, openGraph: { title: destination.name, description: destination.description, images: [destination.image] } } }
2. Static Generation for Popular Routes
export async function generateStaticParams() { const destinations = await prisma.destination.findMany({ where: { popular: true } }) return destinations.map(dest => ({ id: dest.id })) }
3. Edge Runtime for Geo-Location
export const runtime = 'edge' export async function GET(request: NextRequest) { const country = request.geo?.country || 'US' const currency = getCurrencyByCountry(country) return NextResponse.json({ currency }) }
Rate Limiting with Upstash Redis
import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s') }) export async function POST(request: NextRequest) { const ip = request.ip ?? '127.0.0.1' const { success } = await ratelimit.limit(ip) if (!success) { return NextResponse.json( { error: 'Too many requests' }, { status: 429 } ) } // Process request... }
Email Notifications with Resend
import { Resend } from 'resend' const resend = new Resend(process.env.RESEND_API_KEY) async function sendBookingConfirmation(booking: Booking) { await resend.emails.send({ from: 'bookings@bisoatravels.com', to: booking.email, subject: 'Your Booking Confirmation', react: <BookingConfirmationEmail booking={booking} /> }) }
Deployment on Vercel
One command: vercel deploy
Features you get free:
- Automatic HTTPS
- Global CDN
- Preview deployments
- Analytics
- Edge functions
Performance Results
- Lighthouse Score: 98/100
- First Contentful Paint: 0.8s
- Time to Interactive: 1.2s
- Total Blocking Time: 50ms
Lessons Learned
- Server Components are the default - use Client Components sparingly
- Streaming is powerful - don't wait for all data before rendering
- Type safety saves time - Zod + TypeScript catch errors early
- Edge runtime is fast - use it for geolocation, auth checks
- Image optimization matters - Next/Image is non-negotiable
What's Next?
Exploring:
- Parallel Routes for complex UIs
- Intercepting Routes for modals
- Server Actions for forms
- Partial Prerendering (experimental)
Building with Next.js 14? I'd love to see what you're working on! Check out Bisoa Travels for a live example.
Connect:
Let's Build Something Great
I'm open to freelance, full-time, or collaboration opportunities.