Skip to Content
Housr WebappData Layer

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 API

postFormData<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" (the DEFAULT_LOCALE)
  • On the client, derives locale from window.location.pathname using getCurrentLocale()

Two API Endpoints

The app communicates with two backend APIs:

APIEnv VarsPurpose
v2 APINEXT_PUBLIC_API_URL_US, NEXT_PUBLIC_API_URL_UKMain REST API for properties, bills, etc.
Legacy v1 APINEXT_PUBLIC_LEGACY_API_URL_US, NEXT_PUBLIC_LEGACY_API_URL_UKOlder 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.

Last updated on