Next.js 14 App Router: Building Production-Ready Travel Platforms

February 1, 2025

10 min read

Next.jsReactTypeScriptWeb Development

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

  1. Server Components are the default - use Client Components sparingly
  2. Streaming is powerful - don't wait for all data before rendering
  3. Type safety saves time - Zod + TypeScript catch errors early
  4. Edge runtime is fast - use it for geolocation, auth checks
  5. 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.