Introduction
This guide is tailored for beginners eager to learn Node.js, Express, and MongoDB by building a practical project: a restaurant reservation system. As a 5th-semester Computer Science student, I've found that hands-on projects are invaluable for understanding backend architecture and database design. This tutorial aims to provide a clear, step-by-step approach to developing such a system.
The system will enable users to view available time slots, book tables for specific dates and times, and allow administrators to manage these reservations. We will also incorporate elements of weekly scheduling to manage restaurant availability efficiently.
Project Scope and Features
The core functionality of our restaurant reservation system includes:
- User-facing: Browse available time slots for a given date.
- User-facing: Book a table, specifying date, time, party size, and contact information.
- Admin-facing: View all upcoming reservations.
- Admin-facing: Manage reservations (e.g., confirm, cancel, edit).
- System: Implement weekly scheduling to define operating hours and availability patterns.
Technology Stack
We will leverage a robust and popular stack for this project:
- Node.js: A JavaScript runtime environment that allows us to build scalable network applications.
- Express.js: A minimalist and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
- MongoDB: A NoSQL document database that is ideal for flexible data structures and rapid development. We'll use it to store reservation details, restaurant schedules, and user information.
Setting Up the Development Environment
Before we dive into coding, ensure you have the following installed:
- Node.js and npm (or Yarn): Download from nodejs.org.
- MongoDB: Download and install from mongodb.com, or use a cloud-hosted service like MongoDB Atlas.
Once installed, create a new project directory and initialize your Node.js project:
mkdir restaurant-reservation-system
cd restaurant-reservation-system
npm init -y
Install the necessary dependencies:
npm install express mongoose dotenv cors
- Express: For building the web server.
- Mongoose: An ODM (Object Data Modeling) library for MongoDB and Node.js, providing schema-based solutions to model application data.
- Dotenv: To load environment variables from a .env file.
- Cors: To enable Cross-Origin Resource Sharing, allowing your frontend to communicate with your backend.
Database Design with MongoDB
We'll define schemas for our data. For a reservation system, key collections might include:
Restaurant Schema
This schema defines the restaurant's basic information and its operating hours.
// schemas/Restaurant.js
const mongoose = require('mongoose');
const scheduleSchema = new mongoose.Schema({
dayOfWeek: { type: String, required: true }, // e.g., 'Monday', 'Tuesday'
openTime: { type: String, required: true }, // e.g., '09:00'
closeTime: { type: String, required: true }, // e.g., '22:00'
isClosed: { type: Boolean, default: false }
});
const restaurantSchema = new mongoose.Schema({
name: { type: String, required: true },
address: { type: String, required: true },
phoneNumber: { type: String, required: true },
operatingHours: [scheduleSchema]
});
module.exports = mongoose.model('Restaurant', restaurantSchema);
Reservation Schema
This schema captures details for each reservation.
// schemas/Reservation.js
const mongoose = require('mongoose');
const reservationSchema = new mongoose.Schema({
restaurantId: { type: mongoose.Schema.Types.ObjectId, ref: 'Restaurant', required: true },
customerName: { type: String, required: true },
customerEmail: { type: String, required: true },
customerPhone: { type: String, required: true },
reservationDate: { type: Date, required: true },
reservationTime: { type: String, required: true }, // e.g., '19:30'
partySize: { type: Number, required: true },
status: {
type: String,
enum: ['Pending', 'Confirmed', 'Cancelled', 'Completed'],
default: 'Pending'
},
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Reservation', reservationSchema);
Building the Express API
We'll set up routes for managing restaurants and reservations. First, create an index.js file for your Express app.
Server Setup (index.js)
// index.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json()); // For parsing application/json
// Database Connection
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB Connected'))
.catch(err => console.error(err));
// Routes (will be defined in separate files)
// Example: app.use('/api/restaurants', require('./routes/restaurants'));
// Example: app.use('/api/reservations', require('./routes/reservations'));
// Basic route
app.get('/', (req, res) => {
res.send('Restaurant Reservation System API is running!');
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Restaurant Routes
Create a routes/restaurants.js file for handling restaurant-related operations.
// routes/restaurants.js
const express = require('express');
const router = express.Router();
const Restaurant = require('../schemas/Restaurant');
// @route POST /api/restaurants
// @desc Add a new restaurant
// @access Public (or Admin)
router.post('/', async (req, res) => {
try {
const newRestaurant = new Restaurant(req.body);
const restaurant = await newRestaurant.save();
res.status(201).json(restaurant);
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
// @route GET /api/restaurants
// @desc Get all restaurants
// @access Public
router.get('/', async (req, res) => {
try {
const restaurants = await Restaurant.find();
res.json(restaurants);
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
// @route GET /api/restaurants/:id
// @desc Get a single restaurant by ID
// @access Public
router.get('/:id', async (req, res) => {
try {
const restaurant = await Restaurant.findById(req.params.id);
if (!restaurant) return res.status(404).json({ message: 'Restaurant not found' });
res.json(restaurant);
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
// Add more routes for updating and deleting restaurants if needed
module.exports = router;
Reservation Routes
Create a routes/reservations.js file for reservation management.
// routes/reservations.js
const express = require('express');
const router = express.Router();
const Reservation = require('../schemas/Reservation');
const Restaurant = require('../schemas/Restaurant');
const mongoose = require('mongoose');
// Helper function to check for conflicts
async function checkConflict(restaurantId, reservationDate, reservationTime, partySize, currentReservationId = null) {
// Basic check: Ensure reservation time is within operating hours for the day
// This is a simplified check. A robust system would need to parse dates and times carefully.
// For now, we assume reservationTime is a string like '19:30'
const restaurant = await Restaurant.findById(restaurantId);
if (!restaurant) {
throw new Error('Restaurant not found');
}
const dateObj = new Date(reservationDate);
const dayOfWeek = dateObj.toLocaleDateString('en-US', { weekday: 'long' });
const operatingHours = restaurant.operatingHours.find(sh => sh.dayOfWeek === dayOfWeek);
if (!operatingHours || operatingHours.isClosed) {
throw new Error('Restaurant is closed on this day.');
}
const [resHour, resMinute] = reservationTime.split(':').map(Number);
const [openHour, openMinute] = operatingHours.openTime.split(':').map(Number);
const [closeHour, closeMinute] = operatingHours.closeTime.split(':').map(Number);
// Convert to minutes for easier comparison
const resTotalMinutes = resHour * 60 + resMinute;
const openTotalMinutes = openHour * 60 + openMinute;
const closeTotalMinutes = closeHour * 60 + closeMinute;
if (resTotalMinutes < openTotalMinutes || resTotalMinutes >= closeTotalMinutes) {
throw new Error('Reservation time is outside operating hours.');
}
// Check for table availability (simplified: assumes fixed capacity per slot)
// In a real system, you'd need table configurations and track capacity.
// For this example, let's assume a restaurant has a total capacity per time slot.
// This requires a more complex query to count existing reservations for the same time/date.
const query = {
restaurantId: new mongoose.Types.ObjectId(restaurantId),
reservationDate: reservationDate,
reservationTime: reservationTime,
status: { $in: ['Pending', 'Confirmed'] } // Consider pending and confirmed reservations
};
if (currentReservationId) {
query._id = { $ne: new mongoose.Types.ObjectId(currentReservationId) }; // Exclude current reservation if updating
}
const conflictingReservations = await Reservation.find(query);
// Simplified capacity check: Let's assume a max of 10 reservations per 30-min slot for demonstration.
// A real system would have detailed table management.
const MAX_SLOT_CAPACITY = 10;
if (conflictingReservations.length >= MAX_SLOT_CAPACITY) {
throw new Error('No available slots at this time. Please choose another time.');
}
return true;
}
// @route POST /api/reservations
// @desc Create a new reservation
// @access Public
router.post('/', async (req, res) => {
const { restaurantId, reservationDate, reservationTime, partySize, customerName, customerEmail, customerPhone } = req.body;
try {
// Validate input
if (!restaurantId || !reservationDate || !reservationTime || !partySize || !customerName || !customerEmail || !customerPhone) {
return res.status(400).json({ message: 'Please provide all required reservation details.' });
}
// Check for conflicts BEFORE saving
await checkConflict(restaurantId, reservationDate, reservationTime, partySize);
const newReservation = new Reservation({
restaurantId,
reservationDate: new Date(reservationDate), // Ensure it's stored as a Date object
reservationTime,
partySize,
customerName,
customerEmail,
customerPhone,
status: 'Confirmed' // Auto-confirm for simplicity in this guide
});
const reservation = await newReservation.save();
res.status(201).json(reservation);
} catch (err) {
console.error(err.message);
res.status(500).json({ message: err.message || 'Server Error' });
}
});
// @route GET /api/reservations
// @desc Get all reservations (Admin view)
// @access Admin
router.get('/', async (req, res) => {
try {
// Add authentication/authorization middleware here for real-world apps
const reservations = await Reservation.find().populate('restaurantId', 'name address');
res.json(reservations);
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
// @route GET /api/reservations/:id
// @desc Get a single reservation by ID
// @access Admin/User (depending on auth)
router.get('/:id', async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id).populate('restaurantId', 'name address');
if (!reservation) return res.status(404).json({ message: 'Reservation not found' });
res.json(reservation);
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
// @route PUT /api/reservations/:id
// @desc Update a reservation (e.g., change status, time)
// @access Admin
router.put('/:id', async (req, res) => {
const { status, reservationDate, reservationTime, partySize } = req.body;
const { id } = req.params;
try {
const reservationToUpdate = await Reservation.findById(id);
if (!reservationToUpdate) return res.status(404).json({ message: 'Reservation not found' });
// If date, time, or party size are changing, re-check conflicts
if (reservationDate || reservationTime || partySize) {
await checkConflict(reservationToUpdate.restaurantId, reservationDate || reservationToUpdate.reservationDate, reservationTime || reservationToUpdate.reservationTime, partySize || reservationToUpdate.partySize, id);
}
const updatedReservation = await Reservation.findByIdAndUpdate(
id,
{ $set: { status, reservationDate, reservationTime, partySize } },
{ new: true } // Return the updated document
);
res.json(updatedReservation);
} catch (err) {
console.error(err.message);
res.status(500).json({ message: err.message || 'Server Error' });
}
});
// @route DELETE /api/reservations/:id
// @desc Cancel a reservation
// @access Admin/User
router.delete('/:id', async (req, res) => {
try {
const reservation = await Reservation.findById(req.params.id);
if (!reservation) return res.status(404).json({ message: 'Reservation not found' });
// In a real app, you might just change status to 'Cancelled'
// For this example, we'll hard delete.
await Reservation.findByIdAndDelete(req.params.id);
res.json({ message: 'Reservation cancelled successfully' });
} catch (err) {
res.status(500).json({ message: 'Server Error' });
}
});
module.exports = router;
Implementing Weekly Scheduling Logic
The Restaurant schema includes an operatingHours array. When a user attempts to book, the backend must verify if the requested date falls within the restaurant's operating days and if the time slot is within the open hours for that specific day. The checkConflict function in the reservations route provides a basic implementation of this, checking the day of the week and time range against the restaurant's defined schedule.
For a more advanced system, you might consider:
- Recurring schedules: Handling holidays or special exceptions.
- Timezone support: Ensuring accurate scheduling across different regions.
- Buffer times: Adding automatic gaps between reservations for cleaning or setup.
Conclusion
This guide has provided a foundational understanding of how to build a restaurant reservation system using Node.js, Express, and MongoDB. We've covered environment setup, database design with Mongoose schemas, and implemented basic API routes for managing restaurants and reservations, including essential conflict checking based on operating hours. This project is a great stepping stone for understanding backend development principles.
