About this project
NestHunt โ real-estate search on the map
A full-stack real-estate portal: browse homes on a synced split-view map, filter geospatially, save favorites, and โ as an agent โ publish your own listings.
Next.js 16React 19TypeScriptTailwind v4MongoDB AtlasMongooseLeafletJWT + bcryptZodVitest
Demo accounts
๐ท๏ธ agent@nesthunt.devโค๏ธ user@nesthunt.devPassword for both: Demo1234!
Scope
- Split-view search: listing cards and an interactive map that stay in sync (hover a card โ its pin highlights, and vice-versa).
- Geospatial search that re-queries as you pan/zoom the map, plus filters for price, beds, type and amenities.
- Immersive listing detail with gallery, specs, description and a neighborhood map.
- JWT auth with two roles: buyers save favorites; agents publish, edit and manage their own listings.
- Fully bilingual (EN/ES) and responsive, with a role-aware guided demo layer.
Architecture
One Next.js app serves both the React UI (with SSR for search and listing pages) and the REST API as Route Handlers, organized in clean layers. MongoDB Atlas stores listings with a geospatial index.
Browser โ React 19 / Next.js clientSSR pages ยท Leaflet map (CARTO tiles) ยท i18n / auth / favorites / tour providers
โ fetch / SSR
Route Handlers (/api/*)route โ service โ repository ยท Zod
โ Mongoose
MongoDB Atlas (Mongoose)2dsphere geo-index ยท $geoWithin
Auth: JWT in an httpOnly cookie (jose) ยท passwords hashed with bcrypt
Design patterns
| Pattern | Where | Why |
|---|---|---|
| Layered API | src/server (route โ service โ repository) | Keep handlers thin; isolate domain rules and data access for testability. |
| Repository + geo-query | property.repository.ts | Encapsulate Mongo and the $geoWithin bounding-box search behind one interface. |
| DTO + Zod validation | server/dto/* | Validate input at the edge and return per-field error messages the UI can show. |
| Provider / Context | components/providers/* | Cross-cutting client state: language, auth, favorites and the guided tour. |
| useSyncExternalStore | language & media-query hooks | SSR-safe external state with no hydration mismatch or setState-in-effect. |
| Optimistic UI | favorites-provider.tsx | Toggle the heart instantly and revert on error for snappy feedback. |
| Server Components + SSR | app/page.tsx, property/[id] | Fast first paint and crawlable listing pages. |
Auth & security
- Stateless JWT (HS256, jose) stored in an httpOnly, SameSite=Lax cookie โ not reachable from JS.
- Passwords hashed with bcrypt; login uses a constant-time compare and a uniform error to avoid leaking which accounts exist.
- RBAC: only agents reach publish/edit endpoints, and an agent can only mutate their own listings (403 on others).
- All input validated server-side with Zod; errors surface per-field, never a generic failure.
Data integrity & domain rules
- Listings belong to one agent; ownership is enforced on every update and delete.
- Draft listings are hidden from public search and only visible to their owner.
- Favorites use a unique (user, property) index, so saving is idempotent โ no duplicates.
- Property coordinates are stored as GeoJSON Points validated to [lng, lat].
Maps & geospatial
- A 2dsphere index powers $geoWithin bounding-box queries as the user pans the map.
- Leaflet with free CARTO tiles (no API key); price pins are custom markers synced to the list on hover.
- Agents set a listing's location by dropping or dragging a pin on a Leaflet map.
- The map is mounted only when visible โ a hidden, zero-size map would otherwise wipe results.
Performance
- Server-side pagination + load-more; map searches are debounced and stale responses discarded.
- Mongoose connection cached on globalThis to survive serverless cold starts (Atlas connection limits).
- Indexed queries (price, beds, type, status, 2dsphere) and lean reads mapped to slim DTOs.
- The Leaflet map is code-split (dynamic import, ssr:false) so it never blocks first paint.
Testing
- Vitest unit tests for the critical domain: JWT sign/verify (round-trip, tamper, garbage), bcrypt hashing, DTO validation, and the geo-filter builder.
- The geo-filter test asserts the exact $geoWithin $box, price range, $all amenities and text $or are constructed correctly.
- An end-to-end Playwright pass exercises the guest โ login โ favorite โ publish flow and fails on any console error.
Conscious trade-offs
- Listing photos are styled gradient placeholders, not hosted images โ it keeps the demo free and on-brand without an image pipeline.
- The API lives in Next.js Route Handlers rather than a separate service; for this scope it keeps one deploy and one type system, with the same clean layering a standalone Node API would have.
- Geospatial search uses a bounding box ($geoWithin $box) rather than radius ($near) because it maps exactly to the visible map rectangle.
- No image upload or payments โ out of scope for a portfolio demo; the architecture leaves clear seams to add them.
Full interviewer deep-dive in docs/TECHNICAL.md.