DEV Community

Cover image for How to Build a Dubai Real Estate Search App with Next.js and the Bayut API
Happy Endpoint
Happy Endpoint

Posted on

How to Build a Dubai Real Estate Search App with Next.js and the Bayut API

In this tutorial, we'll build a full Dubai property search app using Next.js 14 and the Bayut API.

By the end, you'll have a working app with:

  • Location autocomplete
  • Property search with filters
  • Property detail pages
  • Off-plan project listings

Let's build it.

Prerequisites

  • Node.js 18+
  • A RapidAPI account
  • Basic knowledge of Next.js and React

Step 1: Project Setup

npx create-next-app@latest dubai-property-app --typescript --tailwind --app
cd dubai-property-app
npm install axios
Enter fullscreen mode Exit fullscreen mode

Create a .env.local file:

RAPIDAPI_KEY=your_rapidapi_key_here
Enter fullscreen mode Exit fullscreen mode

Get your key by subscribing to the Bayut API:

https://rapidapi.com/happyendpoint/api/bayut14/

Step 2: API Client

Create lib/bayut.ts:

import axios from 'axios';

const client = axios.create({
  baseURL: 'https://bayut14.p.rapidapi.com',
  headers: {
    'x-rapidapi-host': 'bayut14.p.rapidapi.com',
    'x-rapidapi-key': process.env.RAPIDAPI_KEY!
  }
});

export async function searchLocations(query: string) {
  const { data } = await client.get('/autocomplete', {
    params: { query, langs: 'en' }
  });
  return data.data.locations;
}

export async function searchProperties(params: {
  purpose: 'for-sale' | 'for-rent';
  locationId?: string;
  propertyType?: string;
  rooms?: string;
  priceMin?: number;
  priceMax?: number;
  page?: number;
}) {
  const { data } = await client.get('/search-property', {
    params: {
      purpose: params.purpose,
      location_ids: params.locationId,
      property_type: params.propertyType,
      rooms: params.rooms,
      price_min: params.priceMin,
      price_max: params.priceMax,
      sort_order: 'popular',
      page: params.page || 1,
      langs: 'en'
    }
  });
  return data.data;
}

export async function getPropertyDetails(externalId: string) {
  const { data } = await client.get('/property-details', {
    params: { external_id: externalId, langs: 'en' }
  });
  return data.data;
}

export async function searchOffPlan(locationId?: string) {
  const { data } = await client.get('/search-new-projects', {
    params: {
      location_ids: locationId,
      property_type: 'residential',
      sort_order: 'latest',
      page: 1
    }
  });
  return data.data.properties;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Search Page

Create app/page.tsx:

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function HomePage() {
  const [query, setQuery] = useState('');
  const [locations, setLocations] = useState([]);
  const [purpose, setPurpose] = useState<'for-sale' | 'for-rent'>('for-sale');
  const router = useRouter();

  async function handleLocationSearch(value: string) {
    setQuery(value);
    if (value.length < 2) return;

    const res = await fetch(`/api/locations?query=${value}`);
    const data = await res.json();
    setLocations(data);
  }

  function handleSearch(locationId: string) {
    router.push(`/properties?locationId=${locationId}&purpose=${purpose}`);
  }

  return (
    <main className="min-h-screen bg-gradient-to-b from-blue-900 to-blue-700 flex items-center justify-center">
      <div className="bg-white rounded-2xl p-8 w-full max-w-2xl shadow-2xl">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">
          Find Property in Dubai
        </h1>
        <p className="text-gray-500 mb-6">
          Powered by Bayut API β€” real-time UAE property data
        </p>

        <div className="flex gap-2 mb-4">
          <button
            onClick={() => setPurpose('for-sale')}
            className={`px-4 py-2 rounded-lg font-medium ${
              purpose === 'for-sale'
                ? 'bg-blue-600 text-white'
                : 'bg-gray-100 text-gray-700'
            }`}
          >
            Buy
          </button>
          <button
            onClick={() => setPurpose('for-rent')}
            className={`px-4 py-2 rounded-lg font-medium ${
              purpose === 'for-rent'
                ? 'bg-blue-600 text-white'
                : 'bg-gray-100 text-gray-700'
            }`}
          >
            Rent
          </button>
        </div>

        <input
          type="text"
          value={query}
          onChange={(e) => handleLocationSearch(e.target.value)}
          placeholder="Search area, e.g. Dubai Marina..."
          className="w-full border border-gray-200 rounded-xl px-4 py-3 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />

        {locations.length > 0 && (
          <ul className="mt-2 border border-gray-200 rounded-xl overflow-hidden">
            {locations.map((loc: any) => (
              <li
                key={loc.externalID}
                onClick={() => handleSearch(loc.externalID)}
                className="px-4 py-3 hover:bg-blue-50 cursor-pointer flex justify-between"
              >
                <span>{loc.name?.en}</span>
                <span className="text-gray-400 text-sm">
                  {loc.adCount?.toLocaleString()} listings
                </span>
              </li>
            ))}
          </ul>
        )}
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: API Routes

Create app/api/locations/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { searchLocations } from '@/lib/bayut';

export async function GET(req: NextRequest) {
  const query = req.nextUrl.searchParams.get('query') || '';
  const locations = await searchLocations(query);
  return NextResponse.json(locations);
}
Enter fullscreen mode Exit fullscreen mode

Create app/api/properties/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { searchProperties } from '@/lib/bayut';

export async function GET(req: NextRequest) {
  const params = Object.fromEntries(req.nextUrl.searchParams);
  const results = await searchProperties({
    purpose: (params.purpose as 'for-sale' | 'for-rent') || 'for-sale',
    locationId: params.locationId,
    propertyType: params.propertyType,
    rooms: params.rooms,
    priceMin: params.priceMin ? Number(params.priceMin) : undefined,
    priceMax: params.priceMax ? Number(params.priceMax) : undefined,
    page: params.page ? Number(params.page) : 1
  });
  return NextResponse.json(results);
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Property Listing Page

Create app/properties/page.tsx:

import { searchProperties } from '@/lib/bayut';

export default async function PropertiesPage({
  searchParams
}: {
  searchParams: { locationId?: string; purpose?: string; page?: string };
}) {
  const data = await searchProperties({
    purpose: (searchParams.purpose as 'for-sale' | 'for-rent') || 'for-sale',
    locationId: searchParams.locationId,
    page: searchParams.page ? Number(searchParams.page) : 1
  });

  return (
    <div className="max-w-6xl mx-auto px-4 py-8">
      <h2 className="text-2xl font-bold mb-2">
        {data.total.toLocaleString()} Properties Found
      </h2>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
        {data.properties.map((property: any) => (
          <a
            key={property.externalID}
            href={`/property/${property.externalID}`}
            className="bg-white rounded-xl shadow hover:shadow-lg transition overflow-hidden"
          >
            {property.coverPhoto && (
              <img
                src={property.coverPhoto.url}
                alt={property.title?.en}
                className="w-full h-48 object-cover"
              />
            )}
            <div className="p-4">
              <p className="font-semibold text-gray-900 truncate">
                {property.title?.en}
              </p>
              <p className="text-blue-600 font-bold text-lg mt-1">
                AED {property.price?.toLocaleString()}
              </p>
              <div className="flex gap-4 text-gray-500 text-sm mt-2">
                <span>{property.rooms} bed</span>
                <span>{property.baths} bath</span>
                <span>{property.area?.toFixed(0)} sqm</span>
              </div>
            </div>
          </a>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Deploy

npm run build
npx vercel --prod
Enter fullscreen mode Exit fullscreen mode

Set RAPIDAPI_KEY in your Vercel environment variables.

What You've Built

A production-ready Dubai property search app with:

  • Real-time data from Bayut via API
  • Location autocomplete
  • Filterable property listings
  • Clean, responsive UI

Total time: ~4-6 hours.

Next Steps

  • Add /transactions for price trend charts
  • Add /agent-search for agent profiles
  • Add /search-new-projects for off-plan section
  • Add saved searches with localStorage or a database

Resources


Happy Endpoint - developer-friendly APIs for real-world data.

Top comments (0)