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
Create a .env.local file:
RAPIDAPI_KEY=your_rapidapi_key_here
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;
}
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>
);
}
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);
}
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);
}
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>
);
}
Step 6: Deploy
npm run build
npx vercel --prod
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
/transactionsfor price trend charts - Add
/agent-searchfor agent profiles - Add
/search-new-projectsfor off-plan section - Add saved searches with localStorage or a database
Resources
- π Bayut API: https://rapidapi.com/happyendpoint/api/bayut14/
- π Docs: https://bayutapi.dev/
- π Happy Endpoint: https://happyendpoint.com
Happy Endpoint - developer-friendly APIs for real-world data.
Top comments (0)