Inside the Build: Engineering a High-Performance Hotel Search Engine
Stack: Next.js 16 · React 19 · TypeScript · Drizzle ORM · PostgreSQL · Redis · TailwindCSS 4
Building a modern travel platform is a different kind of engineering challenge. It's not just about clean UI — though that matters too. It's about designing a system that can absorb the unpredictability of high-latency external APIs, manage complex data hydration pipelines, and still deliver a snappy, trustworthy experience to the user.
This is a deep dive into how I architected the Hotel Search & Booking platform at Aspom Travels — the decisions I made, the patterns I invented, and the tradeoffs I had to own.
Choosing the Stack
Every technical decision flows from priorities. Mine were three: developer velocity, type safety, and performance.
Next.js 16 with the App Router was the natural foundation. Server Components let me push API orchestration and business logic — like price markup calculations — entirely to the server, so the client receives a lean, pre-processed payload. No sensitive net rates exposed to the browser. No unnecessary JavaScript shipped to the user.
Drizzle ORM + PostgreSQL (Neon) handled persistence. Drizzle's zero-overhead philosophy and first-class TypeScript support made it ideal for managing our local cache of hotel metadata — a database that grows smarter with every search.
Redis (via ioredis) became the backbone of our caching strategy. Search is a performance-critical path, and Redis gave us the sub-millisecond read speeds we needed for SERPs and hotel detail queries.
TailwindCSS 4 + Shadcn UI kept the frontend consistent and fast to build — a solid design system with a small CSS footprint.
The Hard Problem: Speed vs. Richness
Here's the fundamental tension with any travel API integration: fetching full hotel data — descriptions, high-res images, amenities, policies — for 500+ hotels in a single request is painfully slow. Users don't wait. They leave.
The naive approach (fetch everything upfront) kills the experience. The opposite (show nothing until it's ready) isn't much better.
My solution was a custom pattern I call Shallow Search with Background Hydration.
How It Works
1. Shallow Fetch on Search When a user submits a search, we make a lightweight call to the RateHawk API. This returns only what we need immediately: hotel IDs and current prices. Fast, focused, minimal.
2. Instant Results via Local Enrichment We immediately cross-reference those IDs against our local PostgreSQL database. Any metadata we already have — names, star ratings, cover images — gets merged in instantly. The user sees results without waiting for an external API to return full payloads.
3. Background Hydration Simultaneously, an async background process identifies which hotels are missing detailed metadata and fetches it from RateHawk. That data is persisted to our Postgres DB and uploaded to AWS S3 for fast future retrieval.
4. A Self-Healing Cache Every subsequent search for the same region becomes richer and faster as the database fills in. The system gets smarter over time — automatically.
// Simplified hydration engine
export async function backgroundHydrate(missingHotels: Hotel[]) {
for (const hotel of missingHotels) {
// Fetch rich metadata from RateHawk
const info = await etgGet("/api/b2b/v3/hotel/info/", { id: hotel.id });
// Persist to DB, upsert on conflict
await db
.insert(hotelsTable)
.values({ ...hotelMetadata })
.onConflictDoUpdate({ target: hotelsTable.id, set: { ...hotelMetadata } });
// Respect API rate limits
await new Promise((resolve) => setTimeout(resolve, 2500));
}
}
```plaintext
The result: a platform that *feels* faster than the raw APIs underneath it.
***
## RateHawk Integration: More Than a Proxy
Wrapping an external API is easy. Building real value on top of it is the actual work.
**Server-Side Price Markups** All pricing logic lives on the server. Before any rate reaches the client, we apply a consistent 15% markup. This keeps net rates private, centralises business logic, and makes margin adjustments a one-line config change rather than a frontend refactor.
**Intelligent Location Search** Using RateHawk's `/multicomplete/` endpoint, I built a location search that categorises results into Regions, Airports, and specific Hotels — a UX pattern similar to what you'd expect from Google Maps. Users get contextual, structured suggestions rather than a flat list.
**Robust Pre-Booking** Before a user ever reaches checkout, we hit a pre-book endpoint to lock in the rate and confirm availability. This eliminates the frustrating "price has changed" error at the payment step — a common failure point in travel booking flows.
***
## Performance & Reliability
**Streaming with Suspense** Search results are streamed using Next.js Suspense boundaries. The layout, filters, and UI chrome render instantly. The hotel list populates as soon as the server-side fetch resolves. Users are never staring at a blank screen.
**Tiered Caching Strategy** Not all data ages at the same rate. I designed caching TTLs accordingly:
* **Location suggestions** → 1-hour cache (stable)
* **Search results pages (SERPs)** → 30-minute cache (moderately volatile)
* **Hotel rate pages** → 15-minute cache (high price volatility)
**Zod Validation on Every API Response** External APIs change without warning. Every RateHawk response is validated through a Zod schema before it touches the application layer. If a field goes missing or a type changes upstream, we catch it gracefully — not with a runtime crash in production.
***
## What I Learned
The most valuable lesson from this project wasn't a specific technology — it was a design philosophy: **move complexity to the background, and give the user the illusion of speed.**
Users don't care how many API calls happen behind the scenes. They care that results appear quickly and that the data is trustworthy. The hydration engine exists entirely to serve that perception. Every architectural decision — Redis caching, server-side markups, Suspense streaming — is in service of the same goal.
Modern full-stack TypeScript development makes this kind of architecture genuinely achievable for a small team. The tooling is fast, the type safety catches whole categories of bugs before they reach production, and the Next.js App Router makes server/client boundaries explicit and manageable.
Next up: how I engineered the booking fulfillment pipeline — from payment capture to voucher generation. Stay tuned.
***
*Built at Aspom Travels · 2026* [*View Portfolio*](https://astridamian.vercel.app/) *·* [*GitHub*](https://github.com/Gabbydamian)