Revive HQ – Real Estate Search (Assessment)
This is a full-stack web application that searches active property listings by single address or city, displays the available results (including photos), and persists user searches + results to Firestore.
Tech stack
- Frontend: Next.js (React + TypeScript + CSS)
- Backend: Node.js + Express (TypeScript)
- Database: Firebase Admin SDK → Firestore
- Listing data source: Repliers API
- Deployment: Vercel + Heroku
Technology choices (why)
- Next.js (App Router): fast iteration, simple routing, and a production-ready build/deploy story for a small demo UI.
- Express API: lightweight HTTP layer, easy to test with Supertest, and keeps provider + persistence logic off the client.
- Firestore (Firebase Admin SDK): quick schema-less persistence for searches + results without managing a DB server.
- Repliers API: provides active listing data with photos and supports sale-only filtering.
Repo structure
- apps/web – Next.js UI
- apps/api – Express API + Firestore persistence
Setup
Prerequisites
- Node.js 20+ recommended
- A Firebase project with Firestore enabled
Clone
git clone https://github.com/marcusmcb/revive-hq-demo.git
cd revive-hq-demoEnvironment variables
This repo includes environment examples for each app:
apps/web/.env.exampleapps/api/.env.example
- Copy env files:
apps/web/.env.local(from theapps/web/.env.examplesection)apps/api/.env(from theapps/api/.env.examplesection)
- Configure Repliers for the API:
- Set
REPLIERS_API_KEYinapps/api/.env(required) - Optionally set
REPLIERS_API_BASE_URL(defaults tohttps://api.repliers.io)
- Configure Firebase Admin for the API (choose one):
- Option A: set
FIREBASE_SERVICE_ACCOUNT_JSON(entire JSON on one line) - Option B: download a Service Account JSON and set
FIREBASE_SERVICE_ACCOUNT_PATH - Option C: set
GOOGLE_APPLICATION_CREDENTIALSand optionallyFIREBASE_PROJECT_ID
Install dependencies
From the repo root:
npm installRun locally (web + api)
npm run dev- Web: http://localhost:3000
- API: http://localhost:4000
Run individually
npm -w apps/api run dev
npm -w apps/web run devBuild + run (production-like)
npm run build
npm run startTesting
This repo uses a small test suite to cover the core workflow (search → results) at a few different levels.
# API + web unit/integration tests
npm test
# API only
npm run test:api
# Web only
npm run test:web
# E2E smoke test (Playwright)
npm run test:e2eNotes:
- The E2E test starts the Next.js dev server automatically.
- The E2E test mocks
POST /v1/search, so it does not require the API to be running or any Repliers/Firebase credentials. - If Playwright prompts you to install browsers, run
npx playwright install.
What we test
- API (Vitest + Supertest): Tests
POST /v1/searchrequest validation and response behavior (success, not-found for address, provider errors). - Provider query (Vitest): Asserts the Repliers integration sends the intended filters (e.g., active listings and
type=sale) so we don’t accidentally include leases/rentals. - Web UI (Vitest + React Testing Library): Checks key UI states and interactions: results header is hidden before search, shows “Searching…” during a request, then renders results, and “Clear Search” resets inputs without changing the selected mode.
- E2E smoke (Playwright): Runs the main city search flow in a real browser to confirm the app wiring works end-to-end.
Why we test
- To catch regressions in the most important user path (searching and viewing listings) without requiring manual clicking.
- To keep integrations stable as provider/API logic evolves (filters, mapping, error handling).
Why these tools
- Vitest: Fast TypeScript-friendly runner used for both Node (API) and jsdom (web) tests.
- Supertest: Makes it simple to call Express routes directly without binding to a real port.
- React Testing Library (jsdom): Tests behavior the way users interact with the UI (labels, buttons, visible states) instead of implementation details.
- Playwright: Provides a lightweight browser-level confidence check; mocking the API keeps the E2E test deterministic and avoids external credentials.
API endpoints
-
GET /health- simple liveness check for the API process.
- quick way to verify the server is up and responding.
-
GET /health/firestore- checks that the API can reach Firestore using the configured credentials.
- helps diagnose Firebase credential/project issues separately from search/provider issues.
-
POST /v1/search- runs a provider search (address or city/state), returns the results, and persists the search + results to Firestore.
- this is the primary “create” workflow for the app.
- Address mode:
{ "mode": "address", "address": "123 Main St, Austin, TX 78701" } - City mode:
{ "mode": "city", "city": "Austin", "state": "TX", "limit": 100 }
Unused but left in place to allow future development
-
GET /v1/searches- lists the most recent saved searches (metadata only).
- supports “read” behavior (recent searches/history) without loading full result sets.
-
GET /v1/searches/:searchId- returns a saved search plus its persisted property results.
- verifies persistence and allows retrieving a past search in a single request.
-
DELETE /v1/searches/:searchId- deletes a saved search and its persisted property documents.
- provides basic cleanup and completes minimal CRUD support.
Firestore schema
searches/{searchId}mode:address | cityquery: original user queryqueryKey: normalized query key (used for recent-search reuse)source:repliersresultCount: number of properties returnedcreatedAt,retrievedAt: timestamps
searches/{searchId}/properties/{sourceId}address,price,beds,baths,sqftphotos:string[]retrievedAt
Optional cache pointer (used to reuse identical recent searches without requiring composite Firestore indexes):
searchCache/{mode}:{queryKey}searchId: most recent search ID for that keyupdatedAt: timestamp
Note: property results are stored in the properties sub-collection (not as fields on the search document). In the Firebase console you’ll see them by opening a search document and then viewing its subcollections. If a search returns 0 results, there will be no property documents to display.
Assumptions
- Photos are stored as URLs in Firestore (not downloaded/rehydrated).
- Address matching is “best effort” via the provider’s keyword search.
Known limitations & next improvements
-
Simple caching is implemented server-side by reusing identical recent searches. This could be expanded with a dedicated TTL/index strategy and cache invalidation.
-
No rate limiting or request queuing. This could be implemented with an IP/user-based token bucket (e.g. rate-limit middleware) plus optional provider-side backpressure via a small in-memory/Redis queue.
-
No image optimization pipeline (currently stored as URLs). This could be implemented by proxying images through the web app/API and generating optimized thumbnails (or using Next.js Image) with caching and size limits.
-
Expanded data resource for additional market coverage and information. This could be implemented by supporting multiple listing sources (or richer provider endpoints) behind a common interface, plus adding pagination and additional filters.
Known Search Locations with results
Repliers sample data coverage is limited. If a city/state search returns 0 results, it's likely not included in the sample dataset provided from their API.
For the purpose of testing the functionality of this project, the following city/state searches should yield results in the UI:
- Denver, CO
- Nashville, TN
- Wichita, KS
- Kansas City, MO
- Salisbury, NC
Deployment (demo)
This project is currently deployed as follows:
- Web (Next.js) → Vercel
- API (Express) → Heroku
Web on Vercel (monorepo)
This repo is an npm workspaces monorepo. If your Vercel project Root Directory is set to apps/web, npm won’t see the workspace root by default, which can cause errors like:
npm error No workspaces found: --workspace=apps/web
To make this work reliably, this repo includes apps/web/vercel.json which runs install/build against the repo root (via cd ../..) while still outputting .next from apps/web.
In Vercel project settings:
- Root Directory:
apps/web - Node.js Version:
20.x - Build Command:
npm run build(recommended). If you keep a workspace-based command, usecd ../.. && npm -w apps/web run build. - Install Command:
cd ../.. && npm install(or leave default if you are not using workspaces) - Environment: set
NEXT_PUBLIC_API_BASE_URLto your deployed API URL (e.g. Heroku)
API on Heroku (monorepo)
This repo includes a root Procfile and a heroku-postbuild script so Heroku will:
- build only the API workspace (
npm -w apps/api run build) - start only the API workspace (
npm -w apps/api run start)
On Heroku, set (at minimum) these Config Vars:
REPLIERS_API_KEY- Firebase Admin credentials (one of):
FIREBASE_SERVICE_ACCOUNT_JSON(recommended)- or
FIREBASE_SERVICE_ACCOUNT_PATH/GOOGLE_APPLICATION_CREDENTIALS
CORS_ORIGIN(your Vercel URL)
Web on Vercel
Set NEXT_PUBLIC_API_BASE_URL to your Heroku API URL (e.g. https://<app>.herokuapp.com).
Development Time
- Approximately 11 hours total
Key Additions
- Sort feature on property results in the UI
- "Clear Search" feature in the UI
- Back end testing for key workflows
- Basic caching strategy for search results
- Light/Dark mode usability feature
Key Development Considerations
Data Resource:
In researching real estate API resources for this demo, I encountered a number of options and discovered two in particular that were of note:
- SimplyRETS
- Repliers API
Others that I reviewed (RapidAPI, Reatlor.com, etc) that utilize live MLS data typically require a verified account with a paid enterprise plan, so for the purposes of this demo I proto-typed solutions using both of the APIs listed above.
The SimplyRETS implementation worked without issue but the sample data provided with a developer API key was extremely limited in its search options for this demo.
After some additional research, the Repliers API resource provided a much broader sample data set to demonstrate the search abilities required in this demo.
Monorepo vs separate stand-alone repos:
For the purposes of this demo and to keep the overall directory structure unified, I developed this project as a monorepo.
This also makes it easier to evolve the API contract and UI together (single PR/commit) and keep tooling consistent across the stack (Node/TypeScript versions, linting, and shared test scripts).
For a data-driven project where UI and back end considerations evolve at different paces, separating those considerations into separate repos would (typically) be a better development path.
Separate web + API apps (vs Next.js API routes)
Although Next.js can serve API routes, I kept the Express API in a separate apps/api workspace from the Next.js client in apps/web as requested in the project description and to preserve a clean separation of concerns, making it easy to deploy/scale the API section independently.
This also keeps provider keys and Firebase Admin credentials strictly on the server side and makes API behavior straightforward to test in isolation.
Usability & Saved Search feature
I added the property results "sort" and "clear search" features along with the light/dark mode toggle as simple usability improvements.
While I did scaffold the necessary API endpoints on the back end, I did not implement a "recent searches" feature in the UI that would allow users to view and delete previous property searches in the UI.