Next.js Integration Guide
Build a property listing application with the Dubai Real Estate Data API using Next.js App Router, Route Handlers, and React Server Components.
Overview
The Dubai Real Estate Data API provides comprehensive access to Dubai's property market data, sourced directly from the Dubai Land Department (DLD). With this integration guide, you can build powerful property search, analytics, and listing applications using Next.js.
Transactions
Total Value
Areas
Projects
API Base URL
https://api.buyorsell24.comGetting an API Key
All API requests require a Bearer token for authentication. Follow these steps to obtain your key:
- 1
Visit the Contact Page
Go to dynamicweblab.com/contact and fill out the form requesting API access.
- 2
Receive Your Token
You will receive a Bearer token via email within 24 hours.
- 3
Store It Securely
Add your token to your environment variables. Never commit API keys to version control.
Project Setup
Create a New Next.js Project
npx create-next-app@latest dubai-property-app
# Choose TypeScript, Tailwind CSS, App Router, src/ directoryInstall Dependencies
npm install next-intlEnvironment Variables
Create a .env.local file in your project root:
# .env.local
NEXT_PUBLIC_API_BASE_URL=https://api.buyorsell24.com
API_KEY=your_bearer_token_hereAPI Route Handler Pattern
Use Next.js Route Handlers to proxy API requests, keeping your API key server-side and away from the client bundle.
Create a Route Handler
File: src/app/api/transactions/route.ts
import { NextRequest, NextResponse } from 'next/server';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const params = new URLSearchParams();
searchParams.forEach((value, key) => {
params.set(key, value);
});
const response = await fetch(
`${API_BASE}/api/v1/transactions?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
next: { revalidate: 3600 },
}
);
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch transactions' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
}Areas Route Handler
File: src/app/api/areas/route.ts
import { NextRequest, NextResponse } from 'next/server';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const params = new URLSearchParams();
searchParams.forEach((value, key) => {
params.set(key, value);
});
const response = await fetch(
`${API_BASE}/api/v1/areas?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
next: { revalidate: 86400 },
}
);
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch areas' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
}Projects Route Handler
File: src/app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const params = new URLSearchParams();
searchParams.forEach((value, key) => {
params.set(key, value);
});
const response = await fetch(
`${API_BASE}/api/v1/projects?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
next: { revalidate: 3600 },
}
);
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch projects' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
}Client-Side Data Fetching
Server ComponentDefault (no directive needed)
Server Components fetch data directly on the server. No API key exposure, no client-side JavaScript.
import { TransactionList } from '@/components/TransactionList';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
async function getTransactions() {
const response = await fetch(
`${API_BASE}/api/v1/transactions?limit=20`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
next: { revalidate: 3600 },
}
);
if (!response.ok) {
throw new Error('Failed to fetch transactions');
}
return response.json();
}
export default async function TransactionsPage() {
const data = await getTransactions();
return (
<main>
<h1>Recent Transactions</h1>
<TransactionList transactions={data.transactions} />
{data.next_cursor && (
<a href={`?cursor=${data.next_cursor}`}>Load more</a>
)}
</main>
);
}"use client"Client Component
For interactive features like search, filters, and real-time updates, use a Client Component that calls your Route Handler.
'use client';
import { useState, useEffect } from 'react';
interface Transaction {
id: string;
property_name: string;
price_aed: number;
area_sqft: number;
transaction_date: string;
location: {
area: string;
building?: string;
};
}
interface ApiResponse {
transactions: Transaction[];
next_cursor: string | null;
}
export function TransactionSearch() {
const [query, setQuery] = useState('');
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSearch() {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({ limit: '20' });
if (query) {
params.set('area', query);
}
const response = await fetch(`/api/transactions?${params}`);
const data: ApiResponse = await response.json();
if (!response.ok) {
throw new Error('Failed to fetch');
}
setTransactions(data.transactions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
return (
<div>
<div className="flex gap-2 mb-4">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by area..."
className="bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-sm"
/>
<button
onClick={handleSearch}
disabled={loading}
className="bg-emerald-500 text-slate-950 px-4 py-2 rounded-lg text-sm font-semibold"
>
{loading ? 'Searching...' : 'Search'}
</button>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
<ul className="space-y-2">
{transactions.map((t) => (
<li key={t.id} className="bg-slate-900 border border-slate-800 rounded-lg p-4">
<div className="font-semibold">{t.property_name}</div>
<div className="text-sm text-slate-400">
AED {t.price_aed.toLocaleString()} · {t.area_sqft} sqft · {t.location.area}
</div>
</li>
))}
</ul>
</div>
);
}Authentication
All requests to the API must include a Bearer token in the Authorization header. Your API key should never be exposed to the client — always use a Route Handler or Server Component.
Do
- ✓Store API keys in environment variables
- ✓Use Route Handlers as server-side proxies
- ✓Use Server Components for data fetching
- ✓Add
.env.localto.gitignore
Don't
- ✗Hardcode API keys in source files
- ✗Expose
NEXT_PUBLIC_prefixed keys with your API key - ✗Pass API keys from client to server components
- ✗Commit environment files to Git
Common Endpoints
/api/v1/transactionsProperty transactions from the Dubai Land Department. Filter by type, area, price range, and date.
const res = await fetch(
'/api/transactions?type=sale&area=Palm Jumeirah&min_price=1000000&limit=10'
);
const { transactions, next_cursor } = await res.json();/api/v1/areasAll 356 Dubai areas with building counts, stats, and project data.
const res = await fetch('/api/areas');
const { areas } = await res.json();/api/v1/projects3,517 projects with stats, buildings, transactions, and price trends.
const res = await fetch('/api/projects?developer=Emaar');
const { projects } = await res.json();/api/v1/developersDeveloper directory with AI-generated profiles, projects, and stats.
const res = await fetch('/api/developers');
const { developers } = await res.json();Error Handling
Always handle API errors gracefully. Here's a robust pattern for your data fetching functions.
Server Component Error Handling
import { ErrorBoundary } from '@/components/ErrorBoundary';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
interface ApiError {
status: number;
message: string;
}
async function fetchWithErrors<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error: ApiError = {
status: response.status,
message: `API Error: ${response.status} ${response.statusText}`,
};
switch (response.status) {
case 401:
error.message = 'Invalid API key. Check your environment variables.';
break;
case 403:
error.message = 'Insufficient permissions for this endpoint.';
break;
case 404:
error.message = 'Resource not found.';
break;
case 429:
error.message = 'Rate limit exceeded. Please wait before retrying.';
break;
case 500:
error.message = 'Server error. Please try again later.';
break;
}
throw error;
}
return response.json();
}
export default async function TransactionsPage() {
try {
const data = await fetchWithErrors<{ transactions: Transaction[] }>(
'/api/v1/transactions?limit=20'
);
return <TransactionList transactions={data.transactions} />;
} catch (error) {
return (
<ErrorBoundary
title="Unable to load transactions"
message={error instanceof Error ? error.message : 'Unknown error occurred'}
/>
);
}
}Client-Side Error Handling
'use client';
import { useState } from 'react';
export function useApi<T>() {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function fetchData(endpoint: string) {
setLoading(true);
setError(null);
try {
const response = await fetch(endpoint);
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(
errorData?.error || `Request failed: ${response.status}`
);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(
err instanceof Error ? err.message : 'An unexpected error occurred'
);
} finally {
setLoading(false);
}
}
return { data, loading, error, fetchData };
}
// Usage
function TransactionPage() {
const { data, loading, error, fetchData } = useApi();
return (
<div>
<button onClick={() => fetchData('/api/transactions?limit=10')}>
Load Transactions
</button>
{loading && <p>Loading...</p>}
{error && <p className="text-red-400">{error}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}Rate Limits
The API enforces rate limits per API key. Implement retry logic with exponential backoff to handle 429 responses.
Rate Limit Tiers
Retry with Exponential Backoff
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000;
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
if (!response.ok) {
lastError = new Error(`HTTP ${response.status}`);
break;
}
return response;
}
throw lastError;
}
// Usage in a Route Handler
export async function GET(request: NextRequest) {
try {
const response = await fetchWithRetry(
`${API_BASE}/api/v1/transactions?${params}`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: 'Service temporarily unavailable' },
{ status: 503 }
);
}
}Pagination
Use the next_cursor field to paginate through large result sets. Pass the cursor as a query parameter to fetch the next page.
Cursor-Based Pagination
// Server Component with pagination
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const API_KEY = process.env.API_KEY;
async function getTransactions(cursor?: string) {
const params = new URLSearchParams({ limit: '20' });
if (cursor) {
params.set('cursor', cursor);
}
const response = await fetch(
`${API_BASE}/api/v1/transactions?${params}`,
{
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
next: { revalidate: 3600 },
}
);
if (!response.ok) {
throw new Error('Failed to fetch transactions');
}
return response.json();
}
export default async function TransactionsPage({
searchParams,
}: {
searchParams: Promise<{ cursor?: string }>;
}) {
const { cursor } = await searchParams;
const data = await getTransactions(cursor);
return (
<main>
<h1>Transactions</h1>
<ul>
{data.transactions.map((t: Transaction) => (
<li key={t.id}>{t.property_name} — AED {t.price_aed}</li>
))}
</ul>
{data.next_cursor && (
<a href={`?cursor=${data.next_cursor}`}>
Load next page →
</a>
)}
</main>
);
}Caching Strategies
Leverage Next.js built-in caching with fetch revalidation and revalidatePath / revalidateTag for optimal performance.
Time-Based Revalidation
// Revalidate every hour (3600 seconds)
const response = await fetch(url, {
next: { revalidate: 3600 },
});
// Revalidate every day (86400 seconds)
const response = await fetch(url, {
next: { revalidate: 86400 },
});
// No caching — always fresh
const response = await fetch(url, {
cache: 'no-store',
});Tag-Based Revalidation
// Assign a tag to the fetch request
const response = await fetch(url, {
next: { tags: ['transactions'] },
});
// Later, revalidate the tag (e.g., via a webhook)
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const body = await request.json();
if (body.event === 'transactions.updated') {
revalidateTag('transactions');
return Response.json({ revalidated: true });
}
return Response.json({ revalidated: false });
}Recommended Revalidation Times
Deployment Considerations
Vercel
- →Add
API_KEYin Project Settings → Environment Variables - →Route Handlers run as serverless functions
- →ISR revalidation works out of the box
- →Edge runtime available for low-latency proxying
Docker / Self-Hosted
- →Pass
API_KEYas environment variable - →Use
next startfor production - →Enable ISR cache with filesystem or Redis
- →Configure
cacheHandlerfor shared cache
Production Checklist
- ✓Environment variables set for production
- ✓
.env.localis in.gitignore - ✓Rate limit retry logic implemented
- ✓Error boundaries wrapping data-dependent components
- ✓Revalidation intervals set appropriately
- ✓Loading states for client-side fetches
Ready to Build?
Get your API key and start integrating Dubai's most comprehensive real estate data into your Next.js application.