1 · Project Overview
Findoorzz is a full-stack rental management platform that streamlines listing, booking, payment and review workflows for PGs and flats. It bridges landlords, brokers, students and long-term renters through a unified portal.
Problems Addressed
- Fragmented listing channels with inconsistent availability
- Manual booking coordination and payment reconciliation
- Lack of transparent tenant reviews and ratings
- Delayed notifications for inquiries and bookings
Key Features
- PG & Flat Listings
Create, search and filter multi-unit accommodations with photos, amenities and location map integration. - Booking Management
Instant booking requests, availability calendars and booking confirmation flows. - Razorpay Payments
Secure, PCI-compliant payment integration for rent, deposits and advance fees (see RAZORPAY_SETUP.md). - Reviews & Ratings
Tenant feedback collection and display to boost trust and transparency. - Real-time Notifications
Email and in-app alerts for new inquiries, booking status changes and payment receipts.
Tech Stack
- Backend: Node.js, Express, MongoDB with Mongoose ORM, PM2 process manager
- Frontend: React, Vite, Tailwind CSS, ESLint & HMR support (see frontend/README.md)
- Deployment: Netlify for frontend, PM2 on VPS for API, Nginx reverse proxy
- Performance: Image optimization, API caching, DB indexing and gzip compression (see PERFORMANCE_OPTIMIZATION.md)
Primary Use Cases
- Landlords publish multiple properties, manage availability and collect online payments.
- Renters search listings, compare amenities and book instantly.
- Students find shared accommodations with roommate matching and review insights.
- Brokers list client properties, track leads and receive commission payouts.
2 · Getting Started
Follow these steps to clone the repo, install dependencies, seed data, and run your first booking flow in the browser.
2.1 Prerequisites
• Node.js v14 or later
• MongoDB (local or remote URI)
• (Optional) Razorpay test account for payment simulation
2.2 Clone & Install
- Clone the repository
git clone https://github.com/VikasPatel2004/Findoorzz.git
cd Findoorzz
- Install backend dependencies
cd backend
npm install
- Install frontend dependencies
cd ../frontend
npm install
2.3 Configure Environment
In backend/
, create a .env
file with:
MONGO_URI=<your MongoDB URI>
JWT_SECRET=<your JWT secret>
EMAIL_USER=<Gmail address for email>
EMAIL_PASS=<Gmail app password or OAuth token>
RAZORPAY_KEY_ID=<your Razorpay key ID>
RAZORPAY_KEY_SECRET=<your Razorpay key secret>
ALLOWED_ORIGINS=http://localhost:3000
2.4 Seed Sample Data
Populate MongoDB with users and listings for testing:
cd backend
npm run seed # Runs scripts/addSampleData.js
You should see console logs confirming users and listings creation.
2.5 Run Development Servers
Open two terminal windows:
Backend
cd backend npm run dev # nodemon index.js, watches for changes
Frontend
cd frontend npm start # starts React app on http://localhost:3000
2.6 First Booking Flow
- In your browser, open http://localhost:3000
- Register or log in with one of the seeded users (check console for credentials)
- Browse “PG Listings” or “Flats” on the home page
- Click View Details on any listing, then Book Now
- In the booking form:
- Enter check-in/check-out dates
- Click Proceed to Payment
- Complete payment using Razorpay’s test card:
• Card Number: 4111 1111 1111 1111
• Expiry: 12/25 CVV: 123 - On success you’ll see a confirmation page and receive an email receipt.
2.7 Troubleshooting
• CORS errors? Ensure ALLOWED_ORIGINS
includes http://localhost:3000
in .env
and restart the backend.
• Database connection fails? Verify MONGO_URI
and that your MongoDB instance is running.
• Seed script didn’t run? Check that scripts/addSampleData.js
is executable and that your .env
is loaded.
• Payment errors? Confirm Razorpay test keys are correct and network access to api.razorpay.com is allowed.
3 · Architecture & Core Concepts
This section explains how Findoorzz’s backend and frontend pieces fit together, highlighting the Express server setup, core data models, authentication flow, payment integration, and optimized image delivery.
Express Server Initialization and Middleware Configuration
Describe how backend/index.js
boots the Express app, layers security/performance middleware, mounts routes, handles errors, and connects to MongoDB.
1. Environment and App Setup
Loads configuration and instantiates Express.
// backend/index.js
require('dotenv').config();
const express = require('express');
const app = express();
2. Security & Performance Middleware
Uses Helmet for secure headers and gzip compression.
const helmet = require('helmet');
const compression = require('compression');
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false
}));
app.use(compression());
3. CORS Configuration
Allows origins from ALLOWED_ORIGINS
or defaults.
const cors = require('cors');
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: ['https://yourapp.com', 'http://localhost:3000'];
app.use(cors({
origin: allowedOrigins,
credentials: true
}));
4. Body Parsing Limits
Caps JSON and URL-encoded payloads at 10 MB.
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
5. Static Asset Caching
Sets one-day cache for common static files.
app.use((req, res, next) => {
if (req.url.match(/\.(css|js|png|jpg|jpeg|gif|ico|svg)$/)) {
res.setHeader('Cache-Control', 'public, max-age=86400');
}
next();
});
6. Mounting API Routes
Each feature exports a router. Example:
// Add a new module ‘reports’
const reportsRoutes = require('./routes/reports');
app.use('/api/reports', reportsRoutes);
// Existing mounts
app.use('/api/auth', require('./routes/auth'));
app.use('/api/listings', require('./routes/listings'));
// … bookings, payments, razorpay, reviews, notifications, user
7. Health Check & Error Handling
Defines /api/health
, 500-handler, and 404-handler.
// Health endpoint
app.get('/api/health', (_, res) =>
res.json({ status: 'OK', timestamp: new Date().toISOString() })
);
// 500-level error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: 'Internal server error',
...(process.env.NODE_ENV === 'development' && { error: err.message })
});
});
// 404 handler
app.use('*', (req, res) =>
res.status(404).json({ message: 'Route not found' })
);
8. MongoDB Connection & Server Start
Configures Mongoose and starts the server outside test
env.
const mongoose = require('mongoose');
const PORT = process.env.PORT || 5000;
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}
module.exports = app;
Booking Model & Polymorphic Listing References
Show how Booking
references both FlatListing
and PGListing
via Mongoose’s refPath
, and how to create/query bookings.
Schema Definition
// backend/models/Booking.js
const mongoose = require('mongoose');
const bookingSchema = new mongoose.Schema({
listingType: {
type: String,
enum: ['FlatListing','PGListing'],
required: true
},
listingId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
refPath: 'listingType'
},
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
bookingStartDate: { type: Date, required: true },
bookingEndDate: { type: Date, required: true },
status: {
type: String,
enum: ['pending','confirmed','cancelled'],
default: 'pending'
},
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Booking', bookingSchema);
Creating a Booking
const Booking = require('./models/Booking');
async function createBooking({ listingId, listingType, userId, startDate, endDate }) {
const booking = new Booking({
listingType,
listingId,
user: userId,
bookingStartDate: startDate,
bookingEndDate: endDate
});
return await booking.save();
}
// Usage
createBooking({
listingId: '60e5f9a6c25e4f3f243a1b89',
listingType: 'FlatListing',
userId: '60e5fb22d4a2c23b14e3d4c5',
startDate: new Date('2025-08-01'),
endDate: new Date('2025-08-31')
});
Querying & Populating
async function getBookingWithDetails(bookingId) {
return await Booking
.findById(bookingId)
.populate('user', 'name email')
.populate('listingId') // resolves against listingType
.lean();
}
Filtering by Listing Type
async function getPgBookings(userId) {
return await Booking
.find({ user: userId, listingType: 'PGListing' })
.populate('listingId')
.exec();
}
Cascade on Listing Removal
const FlatListing = require('./models/FlatListing');
const Booking = require('./models/Booking');
flatListingSchema.pre('remove', async function(next) {
await Booking.updateMany(
{ listingType: 'FlatListing', listingId: this._id },
{ status: 'cancelled' }
);
next();
});
JWT Authentication Middleware
Verify and decode JWTs on incoming requests, attach req.user
, and block unauthorized access.
Middleware Implementation
// backend/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret';
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Access token missing' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: 'Invalid token' });
req.user = user; // { userId, email, ... }
next();
});
}
module.exports = authenticateToken;
Usage in Routes
const express = require('express');
const authenticateToken = require('../middleware/authMiddleware');
const router = express.Router();
router.get('/profile', authenticateToken, (req, res) => {
res.json({ userId: req.user.userId, email: req.user.email });
});
Issuing Tokens
const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: '2h' }
);
res.json({ token });
Razorpay Integration: Order Creation and Payment Verification
Demonstrate backend order creation, signature verification, webhook handling, and a reusable frontend service.
1. Backend Client Initialization
// backend/config/razorpay.js
const Razorpay = require('razorpay');
module.exports = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID,
key_secret: process.env.RAZORPAY_KEY_SECRET
});
2. Backend Routes
// backend/routes/razorpay.js
const router = require('express').Router();
const { body } = require('express-validator');
const crypto = require('crypto');
const razorpay = require('../config/razorpay');
const authenticateToken = require('../middleware/authMiddleware');
// Create order
router.post('/create-order', authenticateToken, [
body('bookingId').notEmpty(),
body('amount').isNumeric()
], async (req, res) => {
const options = {
amount: req.body.amount * 100, // paise
currency: 'INR',
receipt: `rcpt_${Date.now()}`,
notes: { bookingId: req.body.bookingId, userId: req.user.userId }
};
const order = await razorpay.orders.create(options);
// persist Payment record...
res.json({
orderId: order.id,
amount: order.amount,
currency: order.currency
});
});
// Verify payment
router.post('/verify-payment', authenticateToken, [
body('razorpayOrderId').notEmpty(),
body('razorpayPaymentId').notEmpty(),
body('razorpaySignature').notEmpty()
], async (req, res) => {
const { razorpayOrderId, razorpayPaymentId, razorpaySignature } = req.body;
const payload = razorpayOrderId + '|' + razorpayPaymentId;
const expected = crypto
.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
.update(payload)
.digest('hex');
if (expected !== razorpaySignature) {
return res.status(400).json({ message: 'Invalid signature' });
}
// update Payment & Booking statuses...
res.json({ message: 'Payment verified' });
});
// Webhook handler
router.post('/webhook', (req, res) => {
const signature = req.headers['x-razorpay-signature'];
const expected = crypto
.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expected) return res.status(400).end();
const { event, payload } = req.body;
if (event === 'payment.captured') {
// mark completed
}
if (event === 'payment.failed') {
// mark failed
}
res.json({ status: 'ok' });
});
module.exports = router;
3. Frontend Service (razorpayService.js
)
Encapsulates script loading, order creation, modal launch, and verification.
import axios from 'axios';
const API = import.meta.env.VITE_API_BASE_URL;
const loadRazorpay = () =>
new Promise(resolve => {
const script = document.createElement('script');
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
script.onload = () => resolve(window.Razorpay);
document.body.appendChild(script);
});
async function createOrder(bookingId, amount, token) {
return (await axios.post(
`${API}/razorpay/create-order`,
{ bookingId, amount },
{ headers: { Authorization: `Bearer ${token}` } }
)).data;
}
async function verifyPayment(payload, token) {
return (await axios.post(
`${API}/razorpay/verify-payment`,
payload,
{ headers: { Authorization: `Bearer ${token}` } }
)).data;
}
async function processPayment(bookingId, amount, userData, token) {
const { orderId, amount: amt, currency } = await createOrder(bookingId, amount, token);
const Razorpay = await loadRazorpay();
const options = {
key: import.meta.env.VITE_RAZORPAY_KEY_ID,
amount: amt, currency, order_id: orderId,
prefill: { name: userData.name, email: userData.email, contact: userData.phone },
handler: resp => verifyPayment({
razorpayOrderId: resp.razorpay_order_id,
razorpayPaymentId: resp.razorpay_payment_id,
razorpaySignature: resp.razorpay_signature
}, token)
};
const rzp = new Razorpay(options);
rzp.open();
return new Promise(resolve => {
rzp.on('payment.failed', e => resolve({ success: false, error: e.error }));
rzp.on('payment.success', () => resolve({ success: true }));
});
}
export default { processPayment };
4. Usage in React
import razorpayService from '../../services/razorpayService';
async function handlePayment() {
setProcessing(true);
const amount = calculateAmount();
const result = await razorpayService.processPayment(
bookingId, amount, { name: user.name, email: user.email, phone: user.phone }, token
);
if (result.success) {
// success UI
} else {
// error UI
}
setProcessing(false);
}
Using the OptimizedImage Component with Cloudinary
Combine Cloudinary URL transforms with a React component that lazy-loads, handles placeholders, and errors.
1. Configure Cloudinary
// backend/config/cloudinary.js
const cloudinary = require('cloudinary').v2;
require('dotenv').config();
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
module.exports = cloudinary;
2. Generating Transformed URLs
// backend/routes/images.js
const express = require('express');
const router = express.Router();
const cloudinary = require('../config/cloudinary');
router.get('/gallery', async (req, res) => {
const publicIds = ['user_avatar_123', 'product_456', 'banner_789'];
const images = publicIds.map(id => ({
id,
url: cloudinary.url(id, {
width: 800, height: 600, crop: 'fill',
quality: 'auto', fetch_format: 'auto'
})
}));
res.json({ images });
});
module.exports = router;
3. Frontend Rendering
// frontend/src/pages/Gallery.jsx
import React, { useEffect, useState } from 'react';
import OptimizedImage from '../components/OptimizedImage';
function Gallery() {
const [images, setImages] = useState([]);
useEffect(() => {
fetch('/api/images/gallery')
.then(r => r.json())
.then(data => setImages(data.images));
}, []);
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{images.map(({ id, url }) => (
<OptimizedImage
key={id}
src={url}
alt={`Image ${id}`}
className="w-full h-48 object-cover rounded"
placeholder={<div className="h-full bg-gradient-to-r from-gray-200 to-gray-300" />}
onError={() => console.warn(`Failed to load ${id}`)}
/>
))}
</div>
);
}
export default Gallery;
4. Practical Tips
- Use
quality: 'auto'
andfetch_format: 'auto'
for smallest payloads. - Match
width
/height
to layout containers to avoid layout shifts. - Provide low-fidelity placeholders or gradients for perceived performance.
- Leverage
loading="lazy"
and Intersection Observer for pre-loading. - Handle
onError
to display fallback UI or retry logic.
Select a Listings Routes Subsection
Please let me know which part of the listings functionality you’d like documented in Section 4:
• Filtering endpoints (e.g. GET /flat or POST /pg/filtered)
• Image upload & Cloudinary/base64 handling on listings
• Save/unsave listing workflows (bookmarks)
• Generic listing CRUD routes (create, read, update, delete)
• Ownership middleware integration (ensuring only owners can modify)
Reply with one of the above, and I’ll draft the focused subsection.
5 · Frontend Developer Guide
This guide covers reusable React components and utilities shipped with the SPA. Use these patterns to simplify lazy loading, authentication, API caching, and optimized images.
LazyLoader Component
Wrap any React.lazy
component (or nested Suspense children) in <LazyLoader>
to get a consistent loading spinner without repeating boilerplate.
API
Props
children
(ReactNode): components to render once loaded.fallback
(ReactNode, optional): override the default spinner.
Default Spinner
<div className="flex items-center justify-center min-h-[200px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-500"></div>
</div>
Usage
- Lazy-load a page or widget
// src/App.jsx
import React, { lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import LazyLoader from './components/LazyLoader.jsx'
const UserProfile = lazy(() => import('./features/UserProfile'))
export default function App() {
return (
<Routes>
<Route
path="/user/:id"
element={
<LazyLoader>
<UserProfile />
</LazyLoader>
}
/>
</Routes>
)
}
- Custom fallback UI
// src/pages/Dashboard.jsx
import LazyLoader from '../components/LazyLoader.jsx'
export default function Dashboard() {
return (
<LazyLoader fallback={<div>🚀 Loading dashboard...</div>}>
{/* nested lazy components */}
</LazyLoader>
)
}
Tips
- Wrap only the component that lazy-loads.
- Ensure Tailwind is configured in
vite.config.js
. - You can nest multiple
<LazyLoader>
boundaries in your root<App />
. - For SEO-critical routes, consider SSR instead of client Suspense.
PrivateRoute: Enforcing Authentication
Protect routes by redirecting unauthenticated users to /login
.
Component
// src/components/PrivateRoute.jsx
import React, { useContext } from 'react'
import { Navigate } from 'react-router-dom'
import { AuthContext } from '../context/AuthContext.jsx'
export default function PrivateRoute({ children }) {
const { user, loading } = useContext(AuthContext)
if (loading) {
return null // or <Spinner />
}
if (!user) {
return <Navigate to="/login" replace />
}
return children
}
Usage
// src/App.jsx
import { Routes, Route } from 'react-router-dom'
import PrivateRoute from './components/PrivateRoute.jsx'
import Dashboard from './pages/Dashboard.jsx'
import LoginPage from './pages/LoginPage.jsx'
export default function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
</Routes>
)
}
Tips
- Replace
return null
with a spinner for better UX. - Change
<Navigate to="/login" />
to customize the redirect. - Wrap a layout component to guard multiple nested routes.
- Mount
AuthProvider
above your router.
AuthContext: Managing Authentication State
Centralize user profile, JWT token, and auth actions.
Exposed Values
user: object|null
– authenticated user or nulltoken: string|null
– JWT fromlocalStorage
loading: boolean
– true while checking token on mountlogin(email, password): Promise<{success: boolean, message?: string}>
logout(): void
updateUserProfile(data, token): Promise<object>
Setup
// src/index.jsx
import ReactDOM from 'react-dom'
import { AuthProvider } from './context/AuthContext.jsx'
import App from './App.jsx'
ReactDOM.render(
<AuthProvider>
<App />
</AuthProvider>,
document.getElementById('root'),
)
Consume Context
// src/components/LoginForm.jsx
import React, { useContext, useState } from 'react'
import { AuthContext } from '../context/AuthContext.jsx'
export default function LoginForm() {
const { login } = useContext(AuthContext)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
async function handleSubmit(e) {
e.preventDefault()
const result = await login(email, password)
if (!result.success) {
setError(result.message)
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="text-red-500">{error}</div>}
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Log In</button>
</form>
)
}
Global Loading State
// src/App.jsx
import React, { useContext } from 'react'
import { AuthContext } from './context/AuthContext.jsx'
import Spinner from './components/Spinner.jsx'
import AppRoutes from './AppRoutes.jsx'
import PublicRoutes from './PublicRoutes.jsx'
export default function App() {
const { loading, user } = useContext(AuthContext)
if (loading) return <Spinner />
return user ? <AppRoutes /> : <PublicRoutes />
}
Update Profile
// src/components/ProfileEditor.jsx
import React, { useContext, useState } from 'react'
import { AuthContext } from '../context/AuthContext.jsx'
export default function ProfileEditor() {
const { user, token, updateUserProfile } = useContext(AuthContext)
const [formData, setFormData] = useState({ name: user.name, bio: user.bio })
const [status, setStatus] = useState('')
async function save() {
try {
await updateUserProfile(formData, token)
setStatus('Profile updated successfully.')
} catch {
setStatus('Update failed.')
}
}
return (
<div>
{/* form inputs bound to formData */}
<button onClick={save}>Save</button>
{status && <p>{status}</p>}
</div>
)
}
Best Practices
- Always check
loading
before accessinguser
ortoken
. - Handle
login
errors viaresult.message
. - Call
logout()
thennavigate('/login')
if needed. - Include
token
in subsequent API calls.
Caching GET Requests with apiService.get
Fetch and cache GET responses for 5 minutes; retry transient errors up to 3 times with exponential backoff.
Signature
apiService.get(url: string, params?: object, useCache?: boolean): Promise<any>
Basic Usage
// src/services/listingService.js
import { apiService } from './apiService.js'
async function fetchListings() {
const filters = { city: 'Boston', minPrice: 500 }
const listings = await apiService.get('/listings', filters)
console.log(listings)
}
Disable cache for fresh data:
import { apiService } from './services/apiService.js'
const listing = await apiService.get('/listings/12345', {}, false)
console.log(listing)
Clearing Cache
import { apiService } from './services/apiService.js'
// Clear entire cache after CUD operations
apiService.clearCache()
// Clear specific entry
apiService.clearCacheEntry('/listings', { city: 'Boston' })
Advanced Patterns
- Include
page
/limit
inparams
for paginated caching. - Use
useCache=false
for real-time consistency. - Leverage automatic retries on status ≥500.
OptimizedImage: Lazy-Loaded Image with Placeholder
Defer off-screen image loading, show a low-fi placeholder or spinner, and fallback on error.
Props
src
(string, required)alt
(string, required)className
(string)placeholder
(ReactNode)onLoad
(function)onError
(function)- ...Any standard
<img>
attributes
Usage
import OptimizedImage from './components/OptimizedImage.jsx'
// Blurred placeholder component
const BlurPlaceholder = () => (
<div className="w-full h-full bg-gray-200 animate-pulse" />
)
export default function GalleryItem() {
return (
<OptimizedImage
src="https://cdn.example.com/property-123.jpg"
alt="Living room"
className="w-full h-80 object-cover rounded-lg"
placeholder={<BlurPlaceholder />}
onLoad={() => console.log('Image loaded')}
onError={() => console.warn('Failed to load image')}
/>
)
}
Best Practices
- Always provide meaningful
alt
text. - Use a small blurred placeholder to reduce layout shift.
- Forward
loading="lazy"
ordecoding="async"
via spread. - Set container dimensions via
className
to prevent jumps. - Wrap in
<picture>
externally forsrcSet
or art direction.
6 · Configuration & Deployment
This section covers configuring third-party services, building and deploying with zero downtime, securing HTTP headers, and integrating a real-time performance monitor.
6.1 Third-Party Service Configuration
Ensure you set the following environment variables in .env
before starting the server.
6.1.1 Cloudinary (Media Uploads)
Purpose: upload and manage images/files.
Environment variables:
CLOUDINARY_CLOUD_NAME
CLOUDINARY_API_KEY
CLOUDINARY_API_SECRET
Configuration: backend/config/cloudinary.js
const { v2: cloudinary } = require('cloudinary');
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
module.exports = cloudinary;
Usage example:
const cloudinary = require('../config/cloudinary');
async function uploadImage(file, folder = 'user-uploads') {
try {
const { secure_url: url, public_id } = await cloudinary.uploader.upload(file, {
folder,
resource_type: 'auto'
});
return { url, publicId: public_id };
} catch (err) {
console.error('Cloudinary upload error:', err);
throw err;
}
}
Tips:
- Use
resource_type: 'auto'
to handle images/videos. - Persist
publicId
for deletes or transformations.
6.1.2 Email Notifications (Payment Confirmation)
Purpose: send styled payment emails via Gmail.
Environment variables:
EMAIL_USER
EMAIL_PASS
Configuration: backend/config/email.js
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
async function sendPaymentConfirmation(to, name, details) {
const html = `<h1>Hi ${name}</h1>
<p>Payment ${details.transactionId} of ₹${details.amount/100} confirmed.</p>`;
await transporter.sendMail({
from: process.env.EMAIL_USER,
to,
subject: 'Payment Confirmation',
html
});
}
module.exports = { sendPaymentConfirmation };
Usage:
const { sendPaymentConfirmation } = require('../config/email');
async function onPaymentSuccess(user, payment) {
try {
await sendPaymentConfirmation(user.email, user.name, {
transactionId: payment.id,
amount: payment.amount,
createdAt: payment.created_at,
status: payment.status
});
} catch (err) {
// log or retry
}
}
Tips:
- For Gmail, enable “App Passwords” or less-secure apps.
- Customize the HTML template in
email.js
.
6.1.3 Razorpay (Payment Gateway)
Purpose: create and manage orders via Razorpay API.
Environment variables:
RAZORPAY_KEY_ID
RAZORPAY_KEY_SECRET
Configuration: backend/config/razorpay.js
const Razorpay = require('razorpay');
module.exports = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID,
key_secret: process.env.RAZORPAY_KEY_SECRET
});
Usage:
const razorpay = require('../config/razorpay');
async function createOrder(amountInPaise, receiptId) {
const options = {
amount: amountInPaise,
currency: 'INR',
receipt: receiptId,
payment_capture: 1
};
try {
const order = await razorpay.orders.create(options);
return order; // { id, amount, currency, ... }
} catch (err) {
console.error('Razorpay order creation failed:', err);
throw err;
}
}
Tips:
- Convert rupees to paise: ₹500 → 50000.
- Store both your
receiptId
and Razorpay’sorder.id
for reconciliation.
6.2 PM2 Deployment Configuration
Define automated, zero-downtime deployments in ecosystem.config.js
.
module.exports = {
apps: [{
name: 'findoorz-backend',
script: 'server.js',
env_production: {
NODE_ENV: 'production'
}
}],
deploy: {
production: {
user: 'ubuntu',
host: 'your-server-ip',
ref: 'origin/main',
repo: 'git@github.com:VikasPatel2004/Findoorzz.git',
path: '/var/www/findoorz-backend',
'pre-deploy-local': 'npm test',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};
Steps:
- Add SSH key:
ssh-copy-id ubuntu@your-server-ip
- Initial setup:
pm2 deploy ecosystem.config.js production setup
- Deploy changes:
git push origin main
pm2 deploy ecosystem.config.js production
- Rollback:
pm2 deploy ecosystem.config.js production revert <revision>
Best Practices:
- Use SSH repo URLs for passwordless pulls.
- Run tests in
pre-deploy-local
. - Apply migrations in
post-deploy
. - Monitor logs at
~/.pm2
.
Common Gotchas:
- Ensure remote user has write access to
path
. - Match Node/NPM versions between local and server.
- For multi-server, set
host: ['app1', 'app2']
.
6.3 Custom HTTP Headers in netlify.toml
Define security and caching headers at build time.
[build]
publish = "dist"
command = "npm run build:prod"
[build.environment]
NODE_VERSION = "18"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Guidance:
- Apply security headers globally.
- Cache hashed assets for one year with
immutable
. - Test with
netlify dev
.
6.4 Performance Monitor Component
Overlay real-time metrics in development.
Installation & Import
# No extra deps
// src/App.jsx
import React from 'react';
import PerformanceMonitor from './components/PerformanceMonitor';
function App() {
return (
<>
{/* your app */}
<PerformanceMonitor />
</>
);
}
export default App;
Dev-Only Rendering
By default, hides in production. To force in staging:
{process.env.REACT_APP_ENABLE_MONITOR === 'true' && <PerformanceMonitor />}
Metrics Captured
- Load Time:
loadEventEnd - navigationStart
- DOM Ready:
domContentLoadedEventEnd - navigationStart
- FCP:
PerformanceObserver
on'paint'
- LCP:
PerformanceObserver
on'largest-contentful-paint'
Styling & Positioning
<div className="fixed bottom-4 right-4 bg-black bg-opacity-75 text-white p-4 rounded-lg text-xs z-50">…</div>
Override via custom CSS as needed.
Customization
Add CLS observer:
const clsObserver = new PerformanceObserver(list => {
const entry = list.getEntries()[0];
setMetrics(prev => ({ ...prev, cls: entry.value }));
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
Best Practices
- Include only in development bundles.
- Remove or gate before running benchmarks.
- For production, send metrics to an analytics endpoint.
7 · Testing & Quality Assurance
Ensure code correctness, prevent regressions, and surface performance issues during development and CI.
7.1 Setting up MongoDB Test Database with Jest
Ensure each integration test runs against a fresh, isolated MongoDB instance.
File: backend/tests/setupTestDB.js
const mongoose = require('mongoose');
const connectTestDB = async () => {
if (mongoose.connection.readyState === 0) {
const mongoURI = process.env.MONGO_URI_TEST
|| 'mongodb://localhost:27017/findoorz_test';
await mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('Connected to MongoDB test database');
}
};
const closeTestDB = async () => {
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
console.log('Disconnected from MongoDB test database');
}
};
module.exports = { connectTestDB, closeTestDB };
Jest Integration
- Increase timeout in
backend/jest.config.js
:module.exports = { testTimeout: 30000 };
- Use lifecycle hooks in your test suites:
const { connectTestDB, closeTestDB } = require('./setupTestDB'); const request = require('supertest'); const app = require('../src/app'); // Express app describe('Auth API', () => { beforeAll(async () => { await connectTestDB(); }); afterAll(async () => { await closeTestDB(); }); test('Register new user', async () => { const res = await request(app) .post('/api/auth/register') .send({ email: 'a@b.com', password: 'pass123' }); expect(res.status).toBe(201); }); });
Tips
- Set
MONGO_URI_TEST
in CI or local.env
. - Each suite drops the database on teardown—tests start clean.
- Adjust
testTimeout
if CI runs slowly. - To preserve data across suites, skip
dropDatabase()
or call cleanup selectively.
7.2 ESLint Configuration: React Hooks & Fast Refresh
Enforce Hooks rules and catch invalid Fast Refresh exports in React code.
File: frontend/eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]'}],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
];
Installation
npm install --save-dev eslint @eslint/js globals eslint-plugin-react-hooks eslint-plugin-react-refresh
Usage
- Place
eslint.config.js
in project root. - Add to
package.json
:{ "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix" } }
- Run
npm run lint
locally or in CI.
Notes
- For TypeScript, swap in
@typescript-eslint/parser
and plugin configs. - Adjust
no-unused-vars.varsIgnorePattern
for other naming patterns. - This setup targets React 17+ with the new JSX transform.
7.3 PerformanceMonitor Component
Render a live overlay showing page load metrics and Core Web Vitals in development.
File: frontend/src/components/PerformanceMonitor.jsx
import React, { useEffect, useState } from 'react';
const PerformanceMonitor = () => {
const [metrics, setMetrics] = useState({
loadTime: 0,
domContentLoaded: 0,
firstContentfulPaint: 0,
largestContentfulPaint: 0,
});
useEffect(() => {
const measure = () => {
if (window.performance?.timing) {
const t = window.performance.timing;
setMetrics(m => ({
...m,
loadTime: t.loadEventEnd - t.navigationStart,
domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
}));
}
if ('PerformanceObserver' in window) {
new PerformanceObserver(list => {
const entry = list.getEntries().pop();
setMetrics(m => ({ ...m, firstContentfulPaint: entry.startTime }));
}).observe({ entryTypes: ['paint'] });
new PerformanceObserver(list => {
const entry = list.getEntries().pop();
setMetrics(m => ({ ...m, largestContentfulPaint: entry.startTime }));
}).observe({ entryTypes: ['largest-contentful-paint'] });
}
};
if (document.readyState === 'complete') measure();
else window.addEventListener('load', measure);
return () => window.removeEventListener('load', measure);
}, []);
if (import.meta.env.PROD) return null;
return (
<div className="fixed bottom-4 right-4 bg-black bg-opacity-75 text-white p-4 rounded-lg text-xs z-50">
<h3 className="font-bold mb-2">Performance Metrics</h3>
<div className="space-y-1">
<div>Load Time: {metrics.loadTime}ms</div>
<div>DOM Ready: {metrics.domContentLoaded}ms</div>
<div>FCP: {metrics.firstContentfulPaint.toFixed(0)}ms</div>
<div>LCP: {metrics.largestContentfulPaint.toFixed(0)}ms</div>
</div>
</div>
);
};
export default PerformanceMonitor;
Usage
Ensure Tailwind CSS (or adapt styles).
Render early in your root component (
App.jsx
):import React from 'react'; import PerformanceMonitor from './components/PerformanceMonitor'; import MainApp from './MainApp'; function App() { return ( <> <PerformanceMonitor /> <MainApp /> </> ); } export default App;
Run in development. The overlay appears at bottom-right.
Use metrics to catch regressions during local testing.
8 · Contribution Guide
This guide describes how to propose changes, report bugs, and submit new features to the Findoorzz repository. Follow these norms to ensure a smooth review and integration process.
8.1 Forking & Cloning
Fork the repo on GitHub: https://github.com/VikasPatel2004/Findoorzz
Clone your fork locally:
git clone git@github.com:<your-username>/Findoorzz.git cd Findoorzz
8.2 Branch Naming
Use descriptive, type-prefixed branch names:
feat/<feature-name>
for new featuresfix/<bug-description>
for bug fixesdocs/<area>
for documentationchore/<task>
for maintenance
Example:
git checkout -b feat/add-search-filter
8.3 Commit Message Guidelines
Follow a simple conventional style:
<type>(<scope>): <short description>
[optional body]
- type: feat, fix, docs, chore, test
- scope: module or area (e.g., auth, api, ui)
- short description: imperative, max 50 characters
Example:
git commit -m "feat(api): add price range filter to /listings endpoint"
8.4 Coding Style & Linting
- JavaScript/TypeScript: Prettier + ESLint configuration in
.prettierrc
and.eslintrc.js
- Run linters before pushing:
npm install npm run lint # ESLint npm run format # Prettier
8.5 Testing
- Backend: Jest tests located under
backend/tests/
- Frontend: React Testing Library in
frontend/src/__tests__/
- Run tests locally:
npm run test # runs both frontend & backend suites npm run test:watch # watch mode
New features or bug fixes must include appropriate test coverage.
8.6 Documentation Updates
- Update
README.md
for high-level changes - Update inline JSDoc or TypeScript definitions
- For new scripts (e.g.,
addSampleData.js
), add usage instructions underbackend/scripts/README.md
8.7 Pull Request Workflow
- Push your branch to your fork:
git push origin feat/add-search-filter
- Open a PR against
main
with a clear title and description. - Link related issue (e.g., “Closes #42”).
- Ensure all CI checks pass and reviewers are assigned.
- Address feedback promptly; squash “fixup” commits into logical units.
8.8 Review & Merge
- Reviewers check code style, test coverage, and documentation.
- Once approved and CI passes, a maintainer merges via “Squash and merge.”
- Delete your branch after merge.
Thank you for contributing to Findoorzz!