Project Overview and Architecture
Think of our platform as a digital marketplace for properties, similar to how a real estate agency operates. We'll need several key components:
User Management: Like a registration desk at a hotel, handling guest and host accounts
Property Listings: Our digital property catalog
Search System: A sophisticated filter system to find the perfect property
Booking System: Managing reservations and availability
Review System: Building trust through user feedback
Initial Project Setup
mkdir rental_platform
cd rental_platform
npm init -y
# Install core dependencies
npm install express express-session sqlite3 sequelize bcrypt
npm install cookie-parser dotenv multer date-fns
# Install development dependencies
npm install --save-dev sequelize-cli nodemon
Our project structure will mirror the complexity of our application:
rental_platform/
├── config/
│ ├── config.json # Database configuration
│ └── database.js # Sequelize instance
└── dB
│ ├── migrations/ # Database structure versions
│ ├── seeders/ # Initial data
│ └── models/ # Data models
├── routes/ # API endpoints
│ ├── auth.js # Authentication routes
│ ├── properties.js # Property management
│ ├── bookings.js # Booking management
│ └── reviews.js # Review system
├── middleware/ # Custom middleware
├── uploads/ # Property images
├── utils/ # Helper functions
├── .env # Environment variables
└── app.js # Main application file
Database Setup and Configuration
Create your .env file:
# .env
DB_NAME=rental_db
SESSION_SECRET=your_secret_key_here
JWT_SECRET=another_secret_key_here
UPLOAD_PATH=./uploads
Initialize Sequelize:
npx sequelize-cli init
Update config/config.json:
{
"development": {
"dialect": "sqlite",
"storage": "./database.sqlite"
},
"test": {
"dialect": "sqlite",
"storage": "./database.test.sqlite"
},
"production": {
"dialect": "sqlite",
"storage": "./database.production.sqlite"
}
}
Creating Database Migrations
Let's create our database structure. Each migration represents a specific change to our database schema.
Users Table Migration
npx sequelize-cli migration:generate --name create-users-table
// migrations/XXXXXXXXXXXXXX-create-users-table.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
password: {
type: Sequelize.STRING,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
},
isHost: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
phoneNumber: {
type: Sequelize.STRING
},
profileImage: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Users');
}
};
Properties Table Migration
npx sequelize-cli migration:generate --name create-properties-table
// migrations/XXXXXXXXXXXXXX-create-properties-table.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Properties', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
hostId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
},
allowNull: false
},
title: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT
},
type: {
type: Sequelize.STRING, // apartment, house, room
allowNull: false
},
address: {
type: Sequelize.STRING,
allowNull: false
},
city: {
type: Sequelize.STRING,
allowNull: false
},
country: {
type: Sequelize.STRING,
allowNull: false
},
price: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false
},
bedrooms: {
type: Sequelize.INTEGER,
allowNull: false
},
bathrooms: {
type: Sequelize.INTEGER,
allowNull: false
},
maxGuests: {
type: Sequelize.INTEGER,
allowNull: false
},
amenities: {
type: Sequelize.JSON
},
images: {
type: Sequelize.JSON
},
latitude: {
type: Sequelize.FLOAT
},
longitude: {
type: Sequelize.FLOAT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
// Add indexes for search optimization
await queryInterface.addIndex('Properties', ['city']);
await queryInterface.addIndex('Properties', ['price']);
await queryInterface.addIndex('Properties', ['bedrooms']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Properties');
}
};
Bookings Table Migration
npx sequelize-cli migration:generate --name create-bookings-table
// migrations/XXXXXXXXXXXXXX-create-bookings-table.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Bookings', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
propertyId: {
type: Sequelize.INTEGER,
references: {
model: 'Properties',
key: 'id'
},
allowNull: false
},
guestId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
},
allowNull: false
},
checkIn: {
type: Sequelize.DATE,
allowNull: false
},
checkOut: {
type: Sequelize.DATE,
allowNull: false
},
totalPrice: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false
},
guestCount: {
type: Sequelize.INTEGER,
allowNull: false
},
status: {
type: Sequelize.ENUM('pending', 'confirmed', 'cancelled'),
defaultValue: 'pending'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
// Add index for date range queries
await queryInterface.addIndex('Bookings', ['checkIn', 'checkOut']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Bookings');
}
};
Reviews Table Migration
npx sequelize-cli migration:generate --name create-reviews-table
// migrations/XXXXXXXXXXXXXX-create-reviews-table.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Reviews', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
propertyId: {
type: Sequelize.INTEGER,
references: {
model: 'Properties',
key: 'id'
},
allowNull: false
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
},
allowNull: false
},
rating: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
min: 1,
max: 5
}
},
comment: {
type: Sequelize.TEXT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
// Add composite unique constraint
await queryInterface.addConstraint('Reviews', {
fields: ['propertyId', 'userId'],
type: 'unique',
name: 'one_review_per_user_per_property'
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Reviews');
}
};
Creating Database Seeders
Seeders help us populate our database with test data for development.
Users Seeder
npx sequelize-cli seed:generate --name demo-users
// seeders/XXXXXXXXXXXXX-demo-users.js
'use strict';
const bcrypt = require('bcrypt');
module.exports = {
up: async (queryInterface, Sequelize) => {
const hashedPassword = await bcrypt.hash('password123', 10);
await queryInterface.bulkInsert('Users', [
{
name: 'Host User',
email: 'host@example.com',
password: hashedPassword,
isHost: true,
phoneNumber: '+1234567890',
createdAt: new Date(),
updatedAt: new Date()
},
{
name: 'Guest User',
email: 'guest@example.com',
password: hashedPassword,
isHost: false,
phoneNumber: '+1987654321',
createdAt: new Date(),
updatedAt: new Date()
}
]);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.bulkDelete('Users', null, {});
}
};
Properties Seeder
npx sequelize-cli seed:generate --name demo-properties
// seeders/XXXXXXXXXXXXX-demo-properties.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const [users] = await queryInterface.sequelize.query(
`SELECT id FROM Users WHERE isHost = true LIMIT 1;`
);
const hostId = users[0].id;
await queryInterface.bulkInsert('Properties', [
{
hostId: hostId,
title: 'Luxury Downtown Apartment',
description: 'Modern luxury apartment in the heart of the city',
type: 'apartment',
address: '123 Main St',
city: 'New York',
country: 'USA',
price: 199.99,
bedrooms: 2,
bathrooms: 2,
maxGuests: 4,
amenities: JSON.stringify(['wifi', 'parking', 'pool', 'gym']),
images: JSON.stringify([
'apartment1.jpg',
'apartment2.jpg'
]),
latitude: 40.7128,
longitude: -74.0060,
createdAt: new Date(),
updatedAt: new Date()
},
{
hostId: hostId,
title: 'Cozy Beach House',
description: 'Beautiful beachfront property with amazing views',
type: 'house',
address: '456 Ocean Dr',
city: 'Miami',
country: 'USA',
price: 299.99,
bedrooms: 3,
bathrooms: 2,
maxGuests: 6,
amenities: JSON.stringify(['wifi', 'beach-access', 'kitchen', 'parking']),
images: JSON.stringify([
'beach1.jpg',
'beach2.jpg'
]),
latitude: 25.7617,
longitude: -80.1918,
createdAt: new Date(),
updatedAt: new Date()
}
]);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.bulkDelete('Properties', null, {});
}
};
Setting Up Models
Now that we have our database structure and seeders in place, let's create our Sequelize models. Models serve as the blueprint for our application's data, defining not just the structure but also the behavior and relationships between different types of data.
Understanding Models in Our Application
Think of models as intelligent containers for our data. Just as a real estate agent needs to maintain detailed records of properties, clients, and bookings, our models will manage the digital equivalent of these records. Each model not only stores data but also includes methods for validating, transforming, and relating that data to other models.
Setting Up the Database Connection
First, let's create a central database configuration file that our models will use:
// config/database.js
const { Sequelize } = require('sequelize');
const config = require('./config.json')[process.env.NODE_ENV || 'development'];
const sequelize = new Sequelize({
dialect: config.dialect,
storage: config.storage,
logging: false // Set to console.log for debugging
});
module.exports = sequelize;
Creating the Model Index
Next, we'll create an index file to manage all our models and their relationships:
// models/index.js
const sequelize = require('../config/database');
const { DataTypes } = require('sequelize');
// Import models
const User = require('./user');
const Property = require('./property');
const Booking = require('./booking');
const Review = require('./review');
// Define relationships
User.hasMany(Property, {
foreignKey: 'hostId',
as: 'properties'
});
Property.belongsTo(User, {
foreignKey: 'hostId',
as: 'host'
});
User.hasMany(Booking, {
foreignKey: 'guestId',
as: 'bookings'
});
Booking.belongsTo(User, {
foreignKey: 'guestId',
as: 'guest'
});
Property.hasMany(Booking, {
foreignKey: 'propertyId'
});
Booking.belongsTo(Property, {
foreignKey: 'propertyId'
});
Property.hasMany(Review, {
foreignKey: 'propertyId'
});
Review.belongsTo(Property, {
foreignKey: 'propertyId'
});
User.hasMany(Review, {
foreignKey: 'userId'
});
Review.belongsTo(User, {
foreignKey: 'userId'
});
module.exports = {
sequelize,
User,
Property,
Booking,
Review
};
Understanding Model Relationships
Our model relationships mirror real-world connections. For example, just as a real estate agent manages multiple properties, in our system, a User (when acting as a host) can have multiple Properties. Similarly, a Property can have multiple Bookings, just like a real property can have multiple reservations over time.
The relationships we've defined create several automatic methods that Sequelize provides:
- A host can access their properties using: await user.getProperties()
- A property can access its host using: await property.getHost()
- A booking can access its associated property: await booking.getProperty()
- A property can access all its reviews: await property.getReviews()
Model Synchronization
Finally, let's create a utility function to ensure our database is synchronized with our models:
// utils/syncDatabase.js
const { sequelize } = require('../models');
async function syncDatabase() {
try {
await sequelize.authenticate();
console.log('Database connection established successfully.');
// In development, you might want to use sync with force: true
// WARNING: This will drop all tables and recreate them
if (process.env.NODE_ENV === 'development') {
await sequelize.sync({ alter: true });
console.log('Database schema synchronized.');
}
} catch (error) {
console.error('Unable to connect to the database:', error);
process.exit(1);
}
}
module.exports = syncDatabase;
Best Practices for Model Management
When working with models, keep these important principles in mind:
Data Validation: Always validate data at the model level before it reaches the database. This provides a consistent way to ensure data integrity.
Relationship Management: Use the relationship methods Sequelize provides rather than writing raw queries. This helps maintain consistency and prevents errors.
Error Handling: Implement proper error handling in your models to gracefully handle database operations that might fail.
Documentation: Keep your model relationships well-documented, as they form the foundation of your application's data structure.
Next Steps
With our models and their relationships established, we can now move on to creating the routes and controllers that will use these models to handle user requests. In the next section, we'll build the API endpoints for:
Authentication and user management
Property listing and search functionality
Booking creation and management
Review submission and retrieval