Dynamic Web Lab
Integration GuideNext.js 16+

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.

1M+

Transactions

AED 4.37T

Total Value

356

Areas

3,517

Projects

API Base URL

https://api.buyorsell24.com

Getting an API Key

All API requests require a Bearer token for authentication. Follow these steps to obtain your key:

  1. 1

    Visit the Contact Page

    Go to dynamicweblab.com/contact and fill out the form requesting API access.

  2. 2

    Receive Your Token

    You will receive a Bearer token via email within 24 hours.

  3. 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/ directory

Install Dependencies

npm install next-intl

Environment 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_here

API 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.local to .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

GET/api/v1/transactions

Property 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();
GET/api/v1/areas

All 356 Dubai areas with building counts, stats, and project data.

const res = await fetch('/api/areas');
const { areas } = await res.json();
GET/api/v1/projects

3,517 projects with stats, buildings, transactions, and price trends.

const res = await fetch('/api/projects?developer=Emaar');
const { projects } = await res.json();
GET/api/v1/developers

Developer 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

Startup Lite
100
requests / minute
Startup Growth
300
requests / minute
Enterprise
Custom
unlimited

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

Transactions
1 hour (3600s)
Data updates daily from DLD
Areas
24 hours (86400s)
Rarely changes
Projects
6 hours (21600s)
New projects added periodically
Developers
24 hours (86400s)
Directory rarely changes

Deployment Considerations

Vercel

  • Add API_KEY in 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_KEY as environment variable
  • Use next start for production
  • Enable ISR cache with filesystem or Redis
  • Configure cacheHandler for shared cache

Production Checklist

  • Environment variables set for production
  • .env.local is 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.

UAE PDPL
SOC 2 Type II
GDPR
FTA Ready
CCPA