← back to Nationalrealestate
src/server/index.ts
502 lines
/**
* USRealEstate server — Express on :9913, whole-site Basic Auth (CRCP pattern).
* /healthz stays OPEN so deploy smoke-tests + uptime canaries get a 200 without creds.
*/
import 'dotenv/config';
import express from 'express';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { query } from '../../db/pool.ts';
import { mountWatchlistAdmin } from './watchlist_admin.ts';
import { mountProperty } from './property.ts';
import { mountListings } from './listings.ts';
import { mountCommercial } from './commercial.ts';
import { mountSublease } from './sublease.ts';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..', '..');
const PORT = Number(process.env.PORT || 9913);
const app = express();
app.use(express.json({ limit: '256kb' }));
app.get('/healthz', (_req, res) => res.type('text').send('ok'));
// Whole-site Basic Auth — the default admin login, named stakeholder logins, and any
// extra comma-separated user:pass pairs via BASIC_AUTH_EXTRA. Accepts a SET of creds
// (was single-user) so more than one person can be granted access.
const AUTH_PAIRS = [
`${process.env.USRE_USER || 'admin'}:${process.env.USRE_PASS || 'DW2024!'}`,
'Daniel:Dandeana1992',
'Boomer:rentfree911',
'Scott:Russia1980',
].concat((process.env.BASIC_AUTH_EXTRA || '').split(',').map((s) => s.trim()).filter(Boolean));
const ACCEPTED = new Set(AUTH_PAIRS.map((p) => 'Basic ' + Buffer.from(p).toString('base64')));
app.use((req, res, next) => {
if (ACCEPTED.has(req.headers.authorization || '')) return next();
res.set('WWW-Authenticate', 'Basic realm="USRealEstate", charset="UTF-8"');
return res.status(401).send('Authentication required');
});
app.get('/api/health', async (_req, res) => {
try {
const r = await query<{ region_type: string; n: string }>(
`SELECT region_type, COUNT(*)::text AS n FROM region GROUP BY region_type`,
);
const regions: Record<string, number> = {};
for (const row of r.rows) regions[row.region_type] = Number(row.n);
const fresh = await query<{ d: string | null }>(
`SELECT to_char(MAX(period),'YYYY-MM-DD') AS d FROM metric_series WHERE metric='zhvi'`,
);
res.json({ ok: true, regions, data_through: fresh.rows[0]?.d ?? null });
} catch (e: any) {
res.status(500).json({ ok: false, error: String(e.message || e) });
}
});
// Self-correcting engine status — per-source health + recent events + Places quota.
app.get('/api/ingest-health', async (_req, res) => {
try {
const sources = await query(
`SELECT sh.job, sh.state, sh.consecutive_failures, sh.baseline_upserted, sh.last_upserted,
to_char(sh.last_ok_at,'YYYY-MM-DD HH24:MI') AS last_ok_at,
to_char(sh.last_failure_at,'YYYY-MM-DD HH24:MI') AS last_failure_at,
to_char(sh.quarantined_at,'YYYY-MM-DD HH24:MI') AS quarantined_at,
ROUND(EXTRACT(EPOCH FROM (NOW()-sh.last_ok_at))/3600, 1) AS ok_age_h
FROM source_health sh ORDER BY
CASE sh.state WHEN 'quarantined' THEN 0 WHEN 'degraded' THEN 1
WHEN 'retrying' THEN 2 WHEN 'healthy' THEN 3 ELSE 4 END, sh.job`);
const events = await query(
`SELECT job, event, detail, to_char(at,'MM-DD HH24:MI') AS at
FROM source_events ORDER BY id DESC LIMIT 20`);
const pq = await query<{ year_month: string; calls_used: number }>(
`SELECT year_month, calls_used FROM places_quota ORDER BY year_month DESC LIMIT 1`);
// Per-state broker roster health: latest run status/age + live row count per state source.
const brokerStates = await query(
`SELECT s.source, s.brokers,
(SELECT status FROM ingest_runs i WHERE i.source = s.source ORDER BY started_at DESC LIMIT 1) AS last_status,
to_char((SELECT MAX(started_at) FROM ingest_runs i WHERE i.source = s.source AND status='ok'),'MM-DD HH24:MI') AS last_ok,
ROUND(EXTRACT(EPOCH FROM (NOW()-(SELECT MAX(started_at) FROM ingest_runs i WHERE i.source = s.source AND status='ok')))/86400, 1) AS ok_age_d
FROM (SELECT source, COUNT(*)::int AS brokers FROM broker GROUP BY source) s
ORDER BY s.source`);
// Places geographic coverage: metros seeded per West→East tier.
const coverage = await query(
`WITH m AS (SELECT substring(name from ',\\s+([A-Z]{2}(-[A-Z]{2})*)\\s') AS st,
(SELECT COUNT(*) FROM places_seed s WHERE s.region_id=r.id) AS seeds
FROM region r WHERE region_type='metro')
SELECT
CASE WHEN st ~ '(^|-)(CA|OR|WA)($|-)' THEN '1·West Coast'
WHEN st ~ '(^|-)(NV|AZ|ID|UT)($|-)' THEN '2·Interior West'
WHEN st ~ '(^|-)(MT|WY|CO|NM)($|-)' THEN '3·Mountain'
ELSE '4·Rest of US' END AS tier,
COUNT(*) AS metros, COUNT(*) FILTER (WHERE seeds>0) AS started,
COUNT(*) FILTER (WHERE seeds>=60) AS covered
FROM m GROUP BY tier ORDER BY tier`);
res.json({
ok: true,
generated_at: new Date().toISOString(),
sources: sources.rows,
broker_states: brokerStates.rows,
coverage: coverage.rows,
events: events.rows,
places_quota: { ...(pq.rows[0] || { year_month: null, calls_used: 0 }), cap: Number(process.env.PLACES_MONTHLY_CAP || 1500) },
});
} catch (e: any) {
res.status(500).json({ ok: false, error: String(e.message || e) });
}
});
app.get('/health', (_req, res) => res.sendFile(join(ROOT, 'public', 'health.html')));
app.get('/api/regions', async (req, res) => {
try {
const type = String(req.query.type || 'county');
const r = await query(
`SELECT id, region_type, canonical_key, name, state_code, fips, cbsa_code, lat, lng, population
FROM region WHERE region_type = $1 ORDER BY name`,
[type],
);
res.json({ regions: r.rows });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// parcel-covered counties (parcel_source registry), cached — lets geo-search deep-link
// an address to the property page only where we actually have parcels.
let _coveredCache: { at: number; set: Set<string> } | null = null;
async function coveredCounties(): Promise<Set<string>> {
if (_coveredCache && Date.now() - _coveredCache.at < 10 * 60_000) return _coveredCache.set;
try {
const r = await query<{ county_fips: string }>(`SELECT county_fips FROM parcel_source`);
_coveredCache = { at: Date.now(), set: new Set(r.rows.map(x => x.county_fips)) };
} catch { _coveredCache = { at: Date.now(), set: new Set() }; }
return _coveredCache.set;
}
// universal location search — resolves a typed county / city / metro / state to a
// market page. National coverage via the region table (fast; ~4.2k rows). ZIP + exact
// street address are county-scoped and live on the property page (no national ZIP index).
app.get('/api/geo-search', async (req, res) => {
try {
const q = String(req.query.q || '').trim();
if (q.length < 2) return res.json({ q, results: [] });
// ZIP → primary county (zip_county crosswalk). National.
if (/^\d{5}$/.test(q)) {
const r = await query<{ canonical_key: string; name: string; state_code: string | null }>(
`SELECT rg.canonical_key, rg.name, rg.state_code
FROM zip_county zc JOIN region rg ON rg.fips = zc.county_fips AND rg.region_type = 'county'
WHERE zc.zip = $1`, [q]);
if (r.rows.length) { const x = r.rows[0];
return res.json({ q, results: [{ type: 'zip', name: `${x.name} — ZIP ${q}`, state: x.state_code || null,
key: x.canonical_key, url: '/market.html?key=' + encodeURIComponent(x.canonical_key) }] }); }
return res.json({ q, results: [] });
}
// street address (starts with a house number) → Census geocoder → county. National.
if (/^\d+\s+\S/.test(q)) {
try {
const gu = 'https://geocoding.geo.census.gov/geocoder/geographies/onelineaddress?address='
+ encodeURIComponent(q) + '&benchmark=Public_AR_Current&vintage=Current_Current&format=json';
const gj: any = await (await fetch(gu, { signal: AbortSignal.timeout(6000) })).json();
const m = gj?.result?.addressMatches?.[0];
const co = m?.geographies?.Counties?.[0];
if (co?.GEOID) {
const r = await query<{ canonical_key: string; name: string; state_code: string | null }>(
`SELECT canonical_key, name, state_code FROM region WHERE fips = $1 AND region_type = 'county'`, [co.GEOID]);
if (r.rows.length) { const x = r.rows[0];
// property-level intent: if the county has parcel coverage, deep-link into the
// property page prefilled with the address; else fall back to the county market.
const covered = await coveredCounties();
const hasParcels = covered.has(co.GEOID);
const url = hasParcels
? '/property.html?county=' + encodeURIComponent(co.GEOID) + '&q=' + encodeURIComponent(q)
: '/market.html?key=' + encodeURIComponent(x.canonical_key);
return res.json({ q, results: [{ type: 'address', name: m.matchedAddress || q, state: x.state_code || null,
sub: hasParcels ? x.name + ' · view property →' : x.name, key: x.canonical_key, url }] }); }
}
} catch (e) { /* geocoder timeout/failure → fall through to empty (client shows a hint) */ }
return res.json({ q, results: [] });
}
const esc = q.replace(/[%_\\]/g, '\\$&');
const like = '%' + esc + '%';
const starts = esc + '%';
const r = await query<{ region_type: string; canonical_key: string; name: string; state_code: string | null }>(
`SELECT region_type, canonical_key, name, state_code
FROM region
WHERE name ILIKE $1 ESCAPE '\\'
OR (char_length($2) = 2 AND region_type = 'state' AND state_code = upper($2))
ORDER BY (char_length($2) = 2 AND region_type = 'state' AND state_code = upper($2)) DESC,
(name ILIKE $3 ESCAPE '\\') DESC,
CASE region_type WHEN 'county' THEN 0 WHEN 'metro' THEN 1 ELSE 2 END,
population DESC NULLS LAST
LIMIT 12`,
[like, q, starts],
);
res.json({ q, results: r.rows.map(x => ({
type: x.region_type, name: x.name, state: x.state_code || null,
key: x.canonical_key, url: '/market.html?key=' + encodeURIComponent(x.canonical_key),
})) });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// choropleth data: {fips: value} for the latest period of a metric (M2 fills metric_series)
app.get('/api/choropleth', async (req, res) => {
try {
const metric = String(req.query.metric || 'zhvi');
const r = await query<{ fips: string; value: string }>(
`SELECT rg.fips, ms.value::text AS value
FROM metric_series ms
JOIN region rg ON rg.id = ms.region_id AND rg.region_type = 'county'
WHERE ms.metric = $1
AND ms.period = (SELECT MAX(period) FROM metric_series WHERE metric = $1)`,
[metric],
);
const out: Record<string, number> = {};
for (const row of r.rows) if (row.fips) out[row.fips] = Number(row.value);
res.json({ metric, count: r.rows.length, values: out });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// market detail: full 36mo series (all sources) + latest per metric for one region
app.get('/api/market', async (req, res) => {
try {
const key = String(req.query.key || '');
const rg = await query(
`SELECT id, region_type, canonical_key, name, state_code, fips, cbsa_code, lat, lng, population
FROM region WHERE canonical_key = $1`,
[key],
);
if (!rg.rows.length) return res.status(404).json({ error: 'region not found: ' + key });
const region = rg.rows[0];
const r = await query<{ source: string; metric: string; period: string; value: string }>(
`SELECT source, metric, to_char(period, 'YYYY-MM-DD') AS period, value::text AS value
FROM metric_series
WHERE region_id = $1
AND (period >= (CURRENT_DATE - INTERVAL '36 months') OR source = 'acs')
ORDER BY metric, period`,
[region.id],
);
const series: Record<string, [string, number][]> = {};
const latest: Record<string, number> = {};
for (const row of r.rows) {
(series[row.metric] ||= []).push([row.period, Number(row.value)]);
latest[row.metric] = Number(row.value); // rows are period-ASC per metric, so last write wins
}
res.json({ region, series, latest });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
const MARKETS_METRICS = [
'zhvi', 'zhvi_yoy', 'median_sale_price', 'median_sale_price_yoy', 'inventory', 'dom',
'months_of_supply', 'sale_to_list', 'median_hh_income', 'rent_yield', 'affordability',
];
// ranking dashboard: one row per region with latest score + latest key metrics.
// The latest-per-metric scan covers 1.2M rows (~1.8s) — cache per type; data changes monthly.
const marketsCache = new Map<string, { at: number; body: any }>();
const MARKETS_TTL_MS = 5 * 60_000;
app.get('/api/markets', async (req, res) => {
try {
const type = String(req.query.type || 'county');
if (type !== 'county' && type !== 'metro' && type !== 'state') return res.status(400).json({ error: 'type must be county|metro|state' });
const hit = marketsCache.get(type);
if (hit && Date.now() - hit.at < MARKETS_TTL_MS) return res.json(hit.body);
const metricsQ = query<{ region_id: number; metric: string; value: string }>(
`SELECT DISTINCT ON (ms.region_id, ms.metric) ms.region_id, ms.metric, ms.value::text AS value
FROM metric_series ms
JOIN region rg ON rg.id = ms.region_id AND rg.region_type = $1
WHERE ms.metric = ANY($2)
ORDER BY ms.region_id, ms.metric, ms.period DESC`,
[type, MARKETS_METRICS],
);
const scoresQ = query<{ region_id: number; opportunity_score: string; rank: number; components: any }>(
`SELECT rs.region_id, rs.opportunity_score::text AS opportunity_score, rs.rank, rs.components
FROM region_scores rs
JOIN region rg ON rg.id = rs.region_id AND rg.region_type = $1
WHERE rs.score_date = (SELECT MAX(rs2.score_date) FROM region_scores rs2
JOIN region rg2 ON rg2.id = rs2.region_id AND rg2.region_type = $1)`,
[type],
);
const regionsQ = query(
`SELECT id, canonical_key, name, state_code, fips, cbsa_code, population
FROM region WHERE region_type = $1`,
[type],
);
const [metrics, scores, regions] = await Promise.all([metricsQ, scoresQ, regionsQ]);
const metricMap = new Map<number, Record<string, number>>();
for (const m of metrics.rows) {
let o = metricMap.get(m.region_id);
if (!o) { o = {}; metricMap.set(m.region_id, o); }
o[m.metric] = Number(m.value);
}
const scoreMap = new Map(scores.rows.map(s => [s.region_id, s]));
const rows = regions.rows.map((rg: any) => {
const m = metricMap.get(rg.id) || {};
const s = scoreMap.get(rg.id);
const sub = s?.components?.sub_scores || {};
return {
key: rg.canonical_key, name: rg.name, state: rg.state_code, population: rg.population,
opportunity_score: s ? Number(s.opportunity_score) : null,
rank: s?.rank ?? null,
momentum: sub.momentum ?? null, affordability_score: sub.affordability ?? null,
rent_yield_score: sub.rent_yield ?? null, inventory_trend: sub.inventory_trend ?? null,
velocity: sub.velocity ?? null,
data_completeness: s?.components?.data_completeness ?? null,
thin_market: s?.components?.thin_market ?? false,
zhvi: m.zhvi ?? null, zhvi_yoy: m.zhvi_yoy ?? null,
median_sale_price: m.median_sale_price ?? null, median_sale_price_yoy: m.median_sale_price_yoy ?? null,
inventory: m.inventory ?? null, dom: m.dom ?? null,
months_of_supply: m.months_of_supply ?? null, sale_to_list: m.sale_to_list ?? null,
median_hh_income: m.median_hh_income ?? null, rent_yield: m.rent_yield ?? null,
affordability: m.affordability ?? null,
};
});
const body = { type, count: rows.length, rows };
marketsCache.set(type, { at: Date.now(), body });
res.json(body);
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// ---- M-B1 broker registry ----
// paginated broker directory: ?state=&q=&firm=&limit=&offset=
app.get('/api/brokers', async (req, res) => {
try {
const limit = Math.min(Math.max(Number(req.query.limit) || 500, 1), 1000);
const offset = Math.max(Number(req.query.offset) || 0, 0);
const conds: string[] = [];
const params: unknown[] = [];
if (req.query.state) { params.push(String(req.query.state).toUpperCase()); conds.push(`b.license_state = $${params.length}`); }
if (req.query.city) { params.push(String(req.query.city).toUpperCase()); conds.push(`upper(b.city) = $${params.length}`); }
if (req.query.q) { params.push('%' + String(req.query.q) + '%'); conds.push(`(b.name ILIKE $${params.length} OR f.name ILIKE $${params.length} OR b.license_no ILIKE $${params.length})`); }
if (req.query.firm) { params.push('%' + String(req.query.firm) + '%'); conds.push(`f.name ILIKE $${params.length}`); }
if (req.query.status) {
const s = String(req.query.status).toLowerCase();
// convenience: ?status=active matches ACTIVE + "ACTIVE UNDER REVIEW"; else exact-ish match
if (s === 'active') { conds.push(`b.license_status ILIKE 'active%'`); }
else { params.push(s); conds.push(`lower(b.license_status) = $${params.length}`); }
}
const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
params.push(limit, offset);
const r = await query(
`SELECT b.id, b.name, b.license_no, b.license_state, b.license_type, b.license_status,
b.city, b.state_code, b.source, f.name AS firm_name, f.id AS firm_id,
COUNT(*) OVER()::int AS total
FROM broker b LEFT JOIN firm f ON f.id = b.firm_id
${where}
ORDER BY b.name, b.id
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
);
const total = r.rows.length ? Number(r.rows[0].total) : 0;
res.json({ total, limit, offset, rows: r.rows.map(({ total: _t, ...row }: any) => row) });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// firm directory: ?state=&q=&limit=&offset=
app.get('/api/firms', async (req, res) => {
try {
const limit = Math.min(Math.max(Number(req.query.limit) || 500, 1), 1000);
const offset = Math.max(Number(req.query.offset) || 0, 0);
const conds: string[] = [];
const params: unknown[] = [];
if (req.query.state) { params.push(String(req.query.state).toUpperCase()); conds.push(`license_state = $${params.length}`); }
if (req.query.city) { params.push(String(req.query.city).toUpperCase()); conds.push(`upper(hq_city) = $${params.length}`); }
if (req.query.q) { params.push('%' + String(req.query.q) + '%'); conds.push(`(name ILIKE $${params.length} OR license_no ILIKE $${params.length})`); }
const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
params.push(limit, offset);
const r = await query(
`SELECT id, name, license_no, license_state, hq_city, hq_state, agent_count, source,
COUNT(*) OVER()::int AS total
FROM firm
${where}
ORDER BY agent_count DESC NULLS LAST, name
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
);
const total = r.rows.length ? Number(r.rows[0].total) : 0;
res.json({ total, limit, offset, rows: r.rows.map(({ total: _t, ...row }: any) => row) });
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// per-state coverage counts for the brokers.html header bar (+ M-B2 site coverage)
app.get('/api/broker-stats', async (_req, res) => {
try {
const [b, f, s] = await Promise.all([
query<{ license_state: string; n: string }>(`SELECT license_state, COUNT(*)::text AS n FROM broker GROUP BY 1 ORDER BY 1`),
query<{ license_state: string; n: string }>(`SELECT license_state, COUNT(*)::text AS n FROM firm GROUP BY 1 ORDER BY 1`),
query<{ discovered: string; with_url: string; crawled: string; idx: string; contacts: string }>(
`SELECT COUNT(*)::text AS discovered,
COUNT(url)::text AS with_url,
COUNT(*) FILTER (WHERE http_status IS NOT NULL)::text AS crawled,
COUNT(*) FILTER (WHERE has_idx_listings)::text AS idx,
(SELECT COUNT(*) FROM firm_contacts)::text AS contacts
FROM firm_site`),
]);
const firms: Record<string, number> = {};
for (const row of f.rows) firms[row.license_state] = Number(row.n);
const states = b.rows.map(row => ({ state: row.license_state, brokers: Number(row.n), firms: firms[row.license_state] || 0 }));
const sc = s.rows[0];
res.json({
states,
total_brokers: states.reduce((s, x) => s + x.brokers, 0),
total_firms: Object.values(firms).reduce((s, x) => s + x, 0),
site_coverage: {
firms_discovered: Number(sc.discovered),
firms_with_url: Number(sc.with_url),
firms_crawled: Number(sc.crawled),
firms_with_idx: Number(sc.idx),
contact_rows: Number(sc.contacts),
},
});
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// M-B2 firm detail: firm + discovered site + contacts + its brokers (limit 50)
app.get('/api/firm/:id', async (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'bad firm id' });
const [firm, site, contacts, brokers] = await Promise.all([
query(`SELECT id, name, website, phone, hq_city, hq_state, license_no, license_state,
source, agent_count, created_at
FROM firm WHERE id = $1`, [id]),
query(`SELECT url, discovery_method, discovered_at, http_status, title, has_idx_listings,
crawl_status, crawled_at, screenshot_path
FROM firm_site WHERE firm_id = $1`, [id]),
query(`SELECT kind, value, source_url, found_at FROM firm_contacts
WHERE firm_id = $1 ORDER BY kind, value`, [id]),
query(`SELECT id, name, license_no, license_type, license_status, city, state_code
FROM broker WHERE firm_id = $1 ORDER BY name LIMIT 50`, [id]),
]);
if (!firm.rows.length) return res.status(404).json({ error: 'firm not found: ' + id });
res.json({
firm: firm.rows[0],
site: site.rows[0] || null,
contacts: contacts.rows,
brokers: brokers.rows,
});
} catch (e: any) {
res.status(500).json({ error: String(e.message || e) });
}
});
// CSV export of the ranked markets table (analyst pull) — reuses the same
// /api/markets cache-backed dataset via an internal fetch so the shape never drifts.
app.get('/api/markets.csv', async (req, res) => {
try {
const type = String(req.query.type || 'county');
if (type !== 'county' && type !== 'metro' && type !== 'state') return res.status(400).send('bad type');
const r = await fetch(`http://127.0.0.1:${PORT}/api/markets?type=${type}`, {
headers: { authorization: req.headers.authorization || '' },
});
const { rows } = await r.json() as { rows: Record<string, unknown>[] };
if (!rows?.length) return res.status(404).send('no data');
const cols = ['rank', 'name', 'state', 'opportunity_score', 'data_completeness', 'thin_market',
'zhvi', 'zhvi_yoy', 'median_sale_price', 'median_sale_price_yoy', 'inventory', 'dom',
'months_of_supply', 'sale_to_list', 'median_hh_income', 'rent_yield', 'affordability', 'population'];
const esc = (v: unknown) => {
if (v == null) return '';
const s = String(v);
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const csv = [cols.join(',')]
.concat(rows.map(row => cols.map(c => esc(row[c])).join(',')))
.join('\n');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="usrealestate-${type}-markets.csv"`);
res.send(csv);
} catch (e: any) {
res.status(500).send('error: ' + String(e.message || e));
}
});
mountWatchlistAdmin(app);
mountProperty(app);
mountListings(app);
mountCommercial(app);
mountSublease(app);
app.use(express.static(join(ROOT, 'public')));
app.listen(PORT, () => {
console.log(`USRealEstate listening on http://127.0.0.1:${PORT}`);
});