▲ intermediate 25 minutes
Next.js Integration Guide
Build a property search application with Next.js using server components, API routes, and Incremental Static Regeneration powered by BayutAPI.
What you'll build: A Next.js property search app with SSR, API routes, and ISR
Prerequisites: Next.js 14+ · Node.js 18+ · RapidAPI account (free) · Basic Next.js knowledge
Prerequisites
- Node.js 18+ installed
- A RapidAPI account with a BayutAPI subscription (get your free API key)
- Familiarity with Next.js App Router and React Server Components
Installation
Create a new Next.js project:
npx create-next-app@latest my-property-app --app --typescript
cd my-property-app
Add your API key to .env.local:
RAPIDAPI_KEY=your_api_key_here
Authentication Setup
Create a server-side API utility. Since this runs on the server, your API key stays secure:
// src/lib/bayut-api.ts
const BASE_URL = "https://bayut14.p.rapidapi.com";
const headers = {
"x-rapidapi-key": process.env.RAPIDAPI_KEY!,
"x-rapidapi-host": "bayut14.p.rapidapi.com",
};
export async function searchProperties(params: Record<string, string>) {
const queryString = new URLSearchParams(params).toString();
const response = await fetch(`${BASE_URL}/search-property?${queryString}`, {
headers,
next: { revalidate: 3600 }, // Cache for 1 hour (ISR)
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
export async function autocomplete(query: string) {
const params = new URLSearchParams({ query, langs: "en" });
const response = await fetch(`${BASE_URL}/autocomplete?${params}`, {
headers,
next: { revalidate: 86400 }, // Cache for 24 hours
});
if (!response.ok) {
throw new Error(`Autocomplete failed: ${response.status}`);
}
return response.json();
}
Basic Example: Server Component Property Page
Fetch and render properties entirely on the server — no client-side JavaScript needed for the initial render:
// src/app/properties/page.tsx
import { searchProperties } from "@/lib/bayut-api";
interface Property {
id: number;
title: string;
price: number;
bedrooms: number;
bathrooms: number;
area: number;
location: string;
}
export default async function PropertiesPage({
searchParams,
}: {
searchParams: Promise<{ page?: string; location?: string }>;
}) {
const params = await searchParams;
const page = params.page || "1";
const locationId = params.location || "5002";
const data = await searchProperties({
purpose: "for-sale",
location_ids: locationId,
page,
});
const properties: Property[] = data.data.properties;
const { total, totalPages } = data.data;
return (
<main className="max-w-6xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-2">Properties for Sale</h1>
<p className="text-gray-400 mb-8">{total.toLocaleString()} properties found</p>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{properties.map((property) => (
<div key={property.id} className="border border-gray-700 rounded-lg p-4">
<h2 className="font-semibold text-lg">{property.title.en}</h2>
<p className="text-2xl font-bold mt-2">
AED {property.price.toLocaleString()}
</p>
<p className="text-gray-400 mt-1">
{property.bedrooms} bed · {property.bathrooms} bath · {property.area} sqft
</p>
<p className="text-gray-500 text-sm mt-1">{property.location}</p>
</div>
))}
</div>
<div className="flex justify-center gap-4 mt-8">
{Number(page) > 1 && (
<a href={`/properties?page=${Number(page) - 1}&location=${locationId}`}
className="px-4 py-2 border rounded">
Previous
</a>
)}
<span className="px-4 py-2">Page {page} of {totalPages}</span>
{Number(page) < totalPages && (
<a href={`/properties?page=${Number(page) + 1}&location=${locationId}`}
className="px-4 py-2 border rounded">
Next
</a>
)}
</div>
</main>
);
}
API Route for Client-Side Calls
Create an API route to proxy requests from client components. This keeps your API key on the server:
// src/app/api/properties/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const params = new URLSearchParams();
params.set("purpose", searchParams.get("purpose") || "for-sale");
params.set("page", searchParams.get("page") || "1");
const locationId = searchParams.get("location_ids");
if (locationId) params.set("location_ids", locationId);
const minPrice = searchParams.get("price_min");
if (minPrice) params.set("price_min", minPrice);
const maxPrice = searchParams.get("price_max");
if (maxPrice) params.set("price_max", maxPrice);
try {
const response = await fetch(
`https://bayut14.p.rapidapi.com/search-property?${params}`,
{
headers: {
"x-rapidapi-key": process.env.RAPIDAPI_KEY!,
"x-rapidapi-host": "bayut14.p.rapidapi.com",
},
}
);
if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch properties" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Advanced Example: ISR for Property Detail Pages
Use Incremental Static Regeneration to pre-render property pages at build time and refresh them periodically:
// src/app/properties/[id]/page.tsx
import { searchProperties } from "@/lib/bayut-api";
import { notFound } from "next/navigation";
// Revalidate every hour
export const revalidate = 3600;
// Generate static params for the most popular areas
export async function generateStaticParams() {
const popularAreas = ["5001", "5002", "5003"];
const params: { id: string }[] = [];
for (const areaId of popularAreas) {
const data = await searchProperties({
purpose: "for-sale",
location_ids: areaId,
page: "1",
});
for (const property of data.data.properties.slice(0, 5)) {
params.push({ id: String(property.id) });
}
}
return params;
}
export default async function PropertyPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Fetch property details (using search as a lookup)
const data = await searchProperties({
purpose: "for-sale",
page: "1",
});
const property = data.data.properties.find(
(p: { id: number }) => String(p.id) === id
);
if (!property) {
notFound();
}
return (
<main className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">{property.title.en}</h1>
<p className="text-4xl font-bold mt-4">
AED {property.price.toLocaleString()}
</p>
<div className="grid grid-cols-3 gap-4 mt-6 text-center">
<div className="border rounded-lg p-4">
<p className="text-2xl font-bold">{property.bedrooms}</p>
<p className="text-gray-400">Bedrooms</p>
</div>
<div className="border rounded-lg p-4">
<p className="text-2xl font-bold">{property.bathrooms}</p>
<p className="text-gray-400">Bathrooms</p>
</div>
<div className="border rounded-lg p-4">
<p className="text-2xl font-bold">{property.area}</p>
<p className="text-gray-400">Sqft</p>
</div>
</div>
<p className="mt-6 text-gray-400">{property.location}</p>
</main>
);
}
Next Steps
- Read the full API documentation for all query parameters and response fields
- Explore the Property Listing Websites use case for architecture patterns
- Add loading states with Next.js
loading.tsxand Suspense boundaries - Implement search with the autocomplete endpoint using a client component
- Consider using SWR or React Query for client-side data fetching