StayIn is a full-stack rental web platform where users can publish, browse, review and map property listings. It is built with Node.js, Express.js and MongoDB following the MVC architecture for scalability and modularity.
- Listings CRUD — create, read, update and delete property listings.
- Authentication & sessions — secure sign-up / log-in / log-out with Passport.js (
passport-local-mongoose), Express-Session and a MongoDB-backed session store. - Authorization — only the owner of a listing can edit/delete it; only a review's author can delete it.
- Image uploads — listing photos uploaded to Cloudinary via Multer.
- Interactive maps — each listing's location is geocoded and shown on a Mapbox map.
- Reviews & ratings — star ratings and comments on listings.
- Server-side validation — request bodies validated with Joi; flash messages for success/error feedback.
- Server-side rendering — dynamic pages with EJS +
ejs-matelayouts.
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express.js |
| Database | MongoDB + Mongoose |
| Auth | Passport.js, passport-local-mongoose, express-session, connect-mongo |
| File uploads | Multer + multer-storage-cloudinary + Cloudinary |
| Maps | Mapbox (mapbox-gl + @mapbox/mapbox-sdk) |
| Views | EJS + ejs-mate |
| Validation | Joi |
| Misc | method-override, connect-flash, dotenv |
stayin-rental/
├── app.js # App entry point, middleware & route mounting
├── cloudConfig.js # Cloudinary + Multer storage configuration
├── middleware.js # isLoggedIn, isOwner, validation, etc.
├── schema.js # Joi validation schemas
├── models/ # Mongoose models (listing, review, user)
├── controllers/ # Route handlers (listings, reviews, users)
├── routes/ # Express routers (listing, review, user)
├── views/ # EJS templates (listings, users, includes, layouts)
├── public/ # Static assets (css, client-side js incl. map.js)
├── utils/ # ExError (custom error), wrapAsync helper
├── init/ # Database seeding (data.js + index.js)
└── .env # Secrets (NOT committed — see .env.example)
- Node.js (v18+ recommended)
- A MongoDB database — local (
mongod) or a free MongoDB Atlas cluster - A Cloudinary account (free tier)
- A Mapbox account (free tier)
git clone <your-repo-url> stayin
cd stayin
npm installcp .env.example .envThen fill in .env (see Setup guides below for where each value comes from):
ATLAS_DB=<your mongodb connection string>
SECRET=<any long random string>
CLOUD_NAME=<cloudinary cloud name>
CLOUD_API_KEY=<cloudinary api key>
CLOUD_API_SECRET=<cloudinary api secret>
MAP_TOKEN=<mapbox public token>
npm run seednpm run dev # with nodemon (auto-reload) — run: npm i -g nodemon
# or
npm start # plain nodeVisit http://localhost:8080/listings
1. Create a Cloudinary account at https://cloudinary.com and open the Dashboard. Copy your Cloud name, API Key and API Secret into .env:
CLOUD_NAME=...
CLOUD_API_KEY=...
CLOUD_API_SECRET=...
2. Install the packages (already in package.json):
npm install multer cloudinary multer-storage-cloudinary3. Configure Cloudinary storage — cloudConfig.js:
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.CLOUD_API_KEY,
api_secret: process.env.CLOUD_API_SECRET,
});
const storage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'StayIn_DEV', // Cloudinary folder name
allowedFormats: ['png', 'jpg', 'jpeg'],
},
});
module.exports = { cloudinary, storage };4. Wire Multer into the route — in routes/listing.js:
const multer = require('multer');
const { storage } = require('../cloudConfig.js');
const upload = multer({ storage });
// the form field name must be "listing[image]"
router.post('/', upload.single('listing[image]'), wrapAsync(listingController.createListing));5. In the controller (controllers/listings.js) the uploaded file is on req.file:
const url = req.file.path; // Cloudinary URL
const filename = req.file.filename;
newListing.image = { url, filename };6. In the form (views/listings/new.ejs) the form must be multipart/form-data:
<form action="/listings" method="POST" enctype="multipart/form-data">
<input name="listing[image]" type="file" />
</form>1. Create a Mapbox account at https://account.mapbox.com. Under Tokens, copy your Default public token (starts with pk.) into .env:
MAP_TOKEN=pk....
2. Install the server SDK (already in package.json):
npm install @mapbox/mapbox-sdk3. Geocode on the server when a listing is created — controllers/listings.js:
const mbxGeocoding = require('@mapbox/mapbox-sdk/services/geocoding');
const geocodingClient = mbxGeocoding({ accessToken: process.env.MAP_TOKEN });
const response = await geocodingClient
.forwardGeocode({ query: req.body.listing.location, limit: 1 })
.send();
// store GeoJSON Point on the listing
newListing.geometry = response.body.features[0].geometry;The Listing model stores this as a GeoJSON field:
geometry: {
type: { type: String, enum: ['Point'], required: true },
coordinates: { type: [Number], required: true },
}4. Load Mapbox GL on the page — views/layouts/boilerplate.ejs already includes:
<link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js"></script>5. Pass the token + coordinates to the client — in views/listings/show.ejs:
<script>
const mapToken = "<%= process.env.MAP_TOKEN %>";
const listing = <%- JSON.stringify(listing) %>;
</script>
<script src="/js/map.js"></script>6. Render the map — public/js/map.js:
mapboxgl.accessToken = mapToken;
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: listing.geometry.coordinates,
zoom: 9,
});
new mapboxgl.Marker({ color: 'red' })
.setLngLat(listing.geometry.coordinates)
.addTo(map);| Command | Description |
|---|---|
npm start |
Start the server (node app.js) |
npm run dev |
Start with nodemon auto-reload |
npm run seed |
Seed the database with sample listings |
Never commit your real .env. It holds Cloudinary, Mapbox and database secrets. It is already listed in .gitignore.
MIT