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.