Skip to content
Career & Projects⏱️ 18 min read·

Build a Kathmandu Room & Flat Finder Web App in Next.js 14 & MongoDB Atlas

Complete student final-year project guide: Build a localized Kathmandu room rental platform featuring geolocation search, Cloudinary photo uploads, price filtering in NPR, and owner phone verification.

MN
Manoj NeupaneSoftware Developer & Programming Instructor
#Next.js 14#MongoDB Atlas#Geospatial#Final Year Project#Kathmandu#Nepal
🎯 Who This Tutorial Is For

📋 Prerequisites
  • ✓Basic understanding of Next.js 14 App Router and React Server Components
  • ✓A free MongoDB Atlas account with an active M0 cluster
  • ✓A free Cloudinary account for property photo storage

🚀 What You Will Learn & Build

  • ▸Implement MongoDB 2dsphere indexing and $near geospatial queries for Kathmandu valley coordinates
  • ▸Handle server-side image compression and direct upload to Cloudinary without local disk dependency
  • ▸Design accessible search filters for Nepali rent budgets (Under 10k, 10k-20k, 20k+)
  • ▸Confidently defend architectural decisions during college final-year project viva examinations
1

1. Kathmandu Rental Problem & Features

Finding a decent room or 1BHK/2BHK flat in Kathmandu, Lalitpur, or Bhaktapur is notoriously stressful for students moving from outside the valley and young IT professionals. Traditional Facebook groups are flooded with middleman brokers who charge high commissions and share duplicate listings. Building a **Verified Room & Flat Finder for Kathmandu Valley** is one of the highest-scoring, practical final-year capstone projects for **BCA, BSc. CSIT, BIT, and BE Computer** students in Nepal. ### Essential Features Implemented in this Project: - **Neighborhood Search:** Filter listings by Kathmandu hotspots (*New Baneshwor, Koteshwor, Kirtipur, Putalisadak, Tinkune, Kumaripati, Jhamsikhel*). - **Geospatial Distance Query:** Locate rooms within 2 km of colleges (e.g., TU Kirtipur, Pulchowk Campus, St. Xavier's). - **Detailed Nepali Amenities:** 24/7 water supply (*Melamchi or Boring*), solar water heater, bike/car parking, separate electricity sub-meter, kitchen sharing. - **Direct Landlord Contact:** Verified phone numbers with quick WhatsApp call link.
2

2. MongoDB Atlas Geospatial Schema (2dsphere)

To enable location searches (e.g. *"Find rooms within 3km of New Baneshwor"*), MongoDB requires a **GeoJSON Point** field paired with a `2dsphere` index. Here is the Mongoose schema:
src/models/Listing.ts
import mongoose, { Schema, Document, Model } from "mongoose";

export interface IListing extends Document {
  title: string;
  description: string;
  category: "Single Room" | "Double Room" | "Flat 1BHK" | "Flat 2BHK";
  rentNpr: number;
  area: string; // e.g. "New Baneshwor, Kathmandu"
  location: {
    type: "Point";
    coordinates: [number, number]; // [longitude, latitude] -> NOTE: Longitude FIRST in GeoJSON!
  };
  amenities: {
    waterSupply: "Melamchi" | "Boring" | "Jar Water Only";
    parking: "Bike Only" | "Car and Bike" | "None";
    wifiIncluded: boolean;
    separateSubMeter: boolean;
  };
  photos: string[];
  landlord: {
    name: string;
    phone: string;
    isVerified: boolean;
  };
  isAvailable: boolean;
  createdAt: Date;
}

const ListingSchema: Schema = new Schema<IListing>(
  {
    title: { type: String, required: true, trim: true },
    description: { type: String, required: true },
    category: {
      type: String,
      enum: ["Single Room", "Double Room", "Flat 1BHK", "Flat 2BHK"],
      required: true,
    },
    rentNpr: { type: Number, required: true, min: 1000 },
    area: { type: String, required: true },
    location: {
      type: {
        type: String,
        enum: ["Point"],
        default: "Point",
        required: true,
      },
      coordinates: {
        type: [Number], // [longitude, latitude]
        required: true,
      },
    },
    amenities: {
      waterSupply: { type: String, default: "Melamchi" },
      parking: { type: String, default: "Bike Only" },
      wifiIncluded: { type: Boolean, default: false },
      separateSubMeter: { type: Boolean, default: true },
    },
    photos: [{ type: String }],
    landlord: {
      name: { type: String, required: true },
      phone: { type: String, required: true },
      isVerified: { type: Boolean, default: false },
    },
    isAvailable: { type: Boolean, default: true },
  },
  { timestamps: true }
);

// CRITICAL: 2dsphere index for geospatial proximity queries
ListingSchema.index({ location: "2dsphere" });
ListingSchema.index({ rentNpr: 1 });
ListingSchema.index({ area: "text", title: "text" });

export const Listing: Model<IListing> =
  mongoose.models.Listing || mongoose.model<IListing>("Listing", ListingSchema);
3

3. Location-Based Query API (Baneshwor, Kirtipur, Patan)

Here is the Next.js Route Handler supporting geospatial radius queries along with price filters:
src/app/api/listings/route.ts
import { NextRequest, NextResponse } from "next/server";
import { connectToDatabase } from "@/lib/mongodb";
import { Listing } from "@/models/Listing";

export async function GET(req: NextRequest) {
  try {
    await connectToDatabase();

    const { searchParams } = new URL(req.url);
    const lng = searchParams.get("lng");
    const lat = searchParams.get("lat");
    const maxDistanceKm = Number(searchParams.get("radiusKm")) || 5;
    const maxRent = searchParams.get("maxRent");
    const category = searchParams.get("category");
    const search = searchParams.get("search");

    const query: any = { isAvailable: true };

    // Price filter
    if (maxRent) {
      query.rentNpr = { $lte: Number(maxRent) };
    }

    // Category filter
    if (category && category !== "All") {
      query.category = category;
    }

    // Text keyword filter
    if (search) {
      query.$text = { $search: search };
    }

    // Geospatial filter: $near requires [longitude, latitude]
    if (lng && lat) {
      query.location = {
        $near: {
          $geometry: {
            type: "Point",
            coordinates: [parseFloat(lng), parseFloat(lat)],
          },
          $maxDistance: maxDistanceKm * 1000, // converted to meters
        },
      };
    }

    const listings = await Listing.find(query).limit(50).lean();

    return NextResponse.json({ success: true, count: listings.length, data: listings });
  } catch (error: any) {
    console.error("Listing query error:", error);
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }
}
4

4. Next.js Filter & Map Listing Component

Here is the clean listing card with direct WhatsApp dialing and amenity badges:
src/components/rooms/RoomCard.tsx
import Image from "next/image";
import { MapPin, Phone, Droplet, Bike, Wifi } from "lucide-react";

interface ListingProps {
  listing: {
    _id: string;
    title: string;
    category: string;
    rentNpr: number;
    area: string;
    photos: string[];
    amenities: {
      waterSupply: string;
      parking: string;
      wifiIncluded: boolean;
      separateSubMeter: boolean;
    };
    landlord: {
      name: string;
      phone: string;
      isVerified: boolean;
    };
  };
}

export function RoomCard({ listing }: ListingProps) {
  const whatsappUrl = `https://wa.me/977${listing.landlord.phone.replace(/\D/g, "")}?text=Namaste, I saw your room listing for ${encodeURIComponent(listing.title)} in ${encodeURIComponent(listing.area)}. Is it still available?`;

  return (
    <div className="bg-[#F4F4F5] border border-[#E4E4E7] rounded-2xl overflow-hidden hover:border-sky-500/50 transition-all flex flex-col group">
      <div className="relative h-48 w-full bg-slate-800">
        <Image
          src={listing.photos[0] || "/placeholder-room.jpg"}
          alt={listing.title}
          fill
          className="object-cover group-hover:scale-105 transition-transform duration-300"
          sizes="(max-width: 768px) 100vw, 33vw"
        />
        <div className="absolute top-3 left-3 bg-slate-900/80 backdrop-blur-md px-2.5 py-1 rounded-md text-xs font-semibold text-sky-300">
          {listing.category}
        </div>
        <div className="absolute bottom-3 right-3 bg-sky-500 text-slate-950 px-3 py-1 rounded-full text-xs font-bold">
          Rs. {listing.rentNpr.toLocaleString("en-NP")} / month
        </div>
      </div>

      <div className="p-5 flex-1 flex flex-col justify-between">
        <div>
          <div className="flex items-center gap-1.5 text-xs text-slate-400 mb-1">
            <MapPin className="w-3.5 h-3.5 text-sky-400" />
            <span>{listing.area}</span>
          </div>
          <h3 className="text-base font-bold text-slate-100 line-clamp-1 mb-2">{listing.title}</h3>

          <div className="flex flex-wrap gap-2 text-xs text-slate-300 mb-4">
            <span className="flex items-center gap-1 bg-[#E4E4E7] px-2 py-0.5 rounded">
              <Droplet className="w-3 h-3 text-cyan-400" /> {listing.amenities.waterSupply}
            </span>
            <span className="flex items-center gap-1 bg-[#E4E4E7] px-2 py-0.5 rounded">
              <Bike className="w-3 h-3 text-emerald-400" /> {listing.amenities.parking}
            </span>
            {listing.amenities.wifiIncluded && (
              <span className="flex items-center gap-1 bg-[#E4E4E7] px-2 py-0.5 rounded">
                <Wifi className="w-3 h-3 text-amber-400" /> Free WiFi
              </span>
            )}
          </div>
        </div>

        <div className="border-t border-[#E4E4E7] pt-4 flex items-center justify-between">
          <div className="text-xs">
            <span className="text-slate-400 block">Owner</span>
            <span className="font-semibold text-slate-200">{listing.landlord.name}</span>
          </div>
          <a
            href={whatsappUrl}
            target="_blank"
            rel="noopener noreferrer"
            className="flex items-center gap-1.5 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold px-3 py-2 rounded-xl transition-colors"
          >
            <Phone className="w-3.5 h-3.5" />
            <span>WhatsApp Call</span>
          </a>
        </div>
      </div>
    </div>
  );
}

✨ Expected Output & Verification

A deployable real estate portal specifically tailored to Kathmandu valley renting conditions (Melamchi water supply, sub-meters, neighborhood coordinates).

🧪 How to Test the Implementation

  1. 1Create a listing in Baneshwor with coordinates [85.3412, 27.6915]
  2. 2Query listings within 3km of Koteshwor [85.3475, 27.6781] and verify distance ranking
  3. 3Upload test photos and verify optimization as WebP in Cloudinary dashboard
  4. 4Test mobile layout and WhatsApp direct dialing button on smartphone

⚠️ Common Errors & How to Fix Them

❌MongoServerError: Can't extract geo keys: ... coordinates must be valid longitude/latitude

Root Cause: Passing latitude first or providing values outside valid bounds (-180 to 180 for longitude, -90 to 90 for latitude).

Fix: Ensure coordinates are strictly formatted as [longitude, latitude]. For Kathmandu, longitude is ~85.32 and latitude is ~27.71.

❌Vercel upload error: EROFS: read-only file system, open 'uploads/photo.jpg'

Root Cause: Attempting to save multipart file uploads to a local folder like ./public/uploads on serverless Vercel functions.

Fix: Use memory buffers to stream uploads directly to Cloudinary or AWS S3 instead of local disk.

🛡️ Security Considerations & Best Practices

  • !Rate-limit landlord contact lookups to prevent web scrapers from harvesting phone numbers for telemarketing.
  • !Validate uploaded image mime types on the server side (allow only image/jpeg, image/png, image/webp).
  • !Sanitize property descriptions to eliminate Cross-Site Scripting (XSS) vectors.

❓ Frequently Asked Questions (FAQ)

Yes! OpenStreetMap with React-Leaflet is 100% free, requires no credit card, and has comprehensive building footprints across Kathmandu, Patan, and Bhaktapur.

📌 Summary of Key Takeaways

A localized Kathmandu Room & Flat Finder built with Next.js 14, MongoDB Atlas geospatial indexing, and Cloudinary provides an impactful real-world solution and an outstanding portfolio/viva project for Nepali students.

Official References & Further Reading