Data Layer
REST API Helpers
All REST API helpers are exported from src/helpers/api.ts. They resolve the correct API base URL based on locale using getApiUrl(locale) and getLegacyApiUrl(locale) from src/config/config.ts.
get<T>(endpoint, headers?, revalidate?, locale?)
Fetches data from the v2 API. Defaults to revalidate: 3600 (ISR, revalidates every hour).
const data = await get<House>("/properties/123");getWithParams<T>(endpoint, params?, headers?, revalidate?, locale?)
Same as get() but appends URL search params from a Record<string, string>.
const data = await getWithParams<QuoteResponse>(
"/api/bills/quote",
{ postcode: "LS1 1AA", bedrooms: "3" },
{},
0, // revalidate: 0 means no caching
);post<T>(endpoint, body, useLegacyApi?, locale?)
POST with JSON body. The useLegacyApi flag switches between the v2 API and legacy v1 API base URL.
await post("/properties/enquiry", enquiryData); // v2 API
await post("/api/bills/signup", data); // v2 APIpostFormData<T>(endpoint, body, useLegacyApi?, locale?)
POST with application/x-www-form-urlencoded body. Used primarily for legacy API endpoints.
const result = await postFormData(
"/api/external/saveFinixIdentityDetails.php",
data,
true, // useLegacyApi = true
);Locale Resolution
All helpers accept an optional locale parameter. If omitted:
- On the server, defaults to
"us"(theDEFAULT_LOCALE) - On the client, derives locale from
window.location.pathnameusinggetCurrentLocale()
Two API Endpoints
The app communicates with two backend APIs:
| API | Env Vars | Purpose |
|---|---|---|
| v2 API | NEXT_PUBLIC_API_URL_US, NEXT_PUBLIC_API_URL_UK | Main REST API for properties, bills, etc. |
| Legacy v1 API | NEXT_PUBLIC_LEGACY_API_URL_US, NEXT_PUBLIC_LEGACY_API_URL_UK | Older PHP endpoints (Finix identity/payment, etc.) |
The post() and postFormData() helpers accept a useLegacyApi: boolean flag to select which API to call.
Direct MySQL Database
src/lib/db.ts provides a MySQL connection pool via mysql2/promise:
import { query } from "@/lib/db";
const rows = await query<HouseRow>(sql, params);The getPool() function creates a singleton pool with:
- Connection limit: 10
- Port: 3306
- Credentials from
NEXT_PRIVATE_MYSQL_*env vars
Direct DB access is used by the search feature (src/features/search/actions/) for property queries. These queries join the main houses table with the bills database for total-price calculations.
Caching with unstable_cache
Search queries use Next.js unstable_cache for server-side caching:
const getCachedProperties = unstable_cache(
async (city: string) => {
return fetchPropertiesFromDb({ city });
},
["properties"], // cache key prefix
{
revalidate: 10800, // 3 hours
tags: ["properties"], // cache tag for on-demand revalidation
},
);Two cache variants exist:
getCachedProperties— default search (city only, no filters)getCachedPropertiesWithFilters— filtered search (all filter params as cache key)
Map-bounds queries bypass the cache entirely and hit the database directly, since they are highly dynamic.
Server Actions Pattern
Server actions follow a consistent pattern:
"use server";
import { someValidator } from "@/lib/schemas/...";
export async function doSomething(input: InputType) {
try {
// 1. Validate input (often with Zod)
// 2. Call API or DB
// 3. Return success
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}Locations:
- Global actions:
src/app/actions/ - Feature actions:
src/features/*/actions/
Contentstack CMS
Blog posts and location content are fetched from Contentstack (not Sanity — the project migrated). The client is configured in src/lib/contentstack/client.ts:
import contentstack from "@contentstack/delivery-sdk";
const stack = contentstack.stack({
apiKey: process.env.CONTENTSTACK_API_KEY || "",
deliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN || "",
environment: process.env.CONTENTSTACK_ENVIRONMENT || "production",
host: "eu-cdn.contentstack.com",
});Content types include blog_post with a region field (us, uk, or both) for locale filtering.