← Back to Tutorials
Node.js

Build a Restaurant Reservation API

Difficulty: Intermediate Est. Time: ~4 hours

Introduction

APIs (Application Programming Interfaces) are the backbone of modern software development. They allow different applications to communicate with each other - your mobile app talks to your server, your website talks to payment processors, and so on.

In this tutorial, you'll build a complete REST API for a restaurant reservation system. This is the kind of API that a restaurant's website or mobile app would use to manage table bookings.

What You'll Build

A RESTful API for restaurant reservations with:

  • User registration and authentication
  • Table management (create, list tables)
  • Reservation system (book, view, cancel)
  • Input validation
  • Error handling
What You'll Learn
  • REST API design principles
  • Node.js with Express
  • MongoDB with Mongoose
  • JWT authentication
  • Input validation
  • API testing

What is a REST API?

REST (Representational State Transfer) is an architectural style for designing networked applications. It's the most common approach for building web APIs.

REST Principles

  • Stateless - Each request contains all information needed
  • Client-Server - Client and server are separate
  • Uniform Interface - Consistent resource URLs
  • Cacheable - Responses can be cached

HTTP Methods

REST APIs use standard HTTP methods:

  • GET - Retrieve data (read)
  • POST - Create new resources
  • PUT - Update existing resources
  • DELETE - Remove resources

HTTP Status Codes

APIs return status codes to indicate results:

  • 200 - Success
  • 201 - Created (new resource)
  • 400 - Bad request (client error)
  • 401 - Unauthorized
  • 404 - Not found
  • 500 - Server error
REST Example

Here's what a typical REST API looks like:

GET    /api/users          - Get all users
GET    /api/users/123      - Get user with ID 123
POST   /api/users          - Create new user
PUT    /api/users/123      - Update user 123
DELETE /api/users/123      - Delete user 123

Project Overview

Our restaurant reservation API will handle these resources:

Users

  • Register new users (customers)
  • Login and get authentication tokens
  • View user profile

Tables

  • List all tables
  • View table details (capacity, location)
  • See table availability

Reservations

  • Create new reservations
  • View user's reservations
  • Cancel reservations
  • View available time slots

API Endpoints Summary

API Endpoints
# Authentication
POST   /api/auth/register    - Register new user
POST   /api/auth/login      - Login user

# Tables
GET    /api/tables          - List all tables
GET    /api/tables/:id      - Get table details

# Reservations
GET    /api/reservations    - Get user's reservations
POST   /api/reservations    - Create new reservation
DELETE /api/reservations/:id - Cancel reservation

Prerequisites

Before starting this tutorial, ensure you have:

  • Node.js installed - Version 14 or higher
  • MongoDB - Local installation or MongoDB Atlas account
  • API testing tool - Postman or similar
  • Code editor - VS Code recommended
MongoDB Setup

You can either install MongoDB locally or use MongoDB Atlas (cloud). For this tutorial, I'll show how to use MongoDB Atlas as it's easier to set up and works great for learning.

Project Setup

Let's set up our Node.js project and install dependencies.

Initializing the Project

Bash
# Create project directory
mkdir restaurant-api
cd restaurant-api

# Initialize Node.js project
npm init -y

# Install dependencies
npm install express mongoose bcryptjs jsonwebtoken cors dotenv express-validator

# What each package does:
# express           - Web framework
# mongoose         - MongoDB ODM
# bcryptjs         - Password hashing
# jsonwebtoken     - JWT authentication
# cors             - Enable Cross-Origin requests
# dotenv           - Environment variables
# express-validator - Input validation

Project Structure

File Structure
restaurant-api/
├── config/
│   └── db.js            # Database connection
├── middleware/
│   └── auth.js          # Authentication middleware
├── models/
│   ├── User.js          # User model
│   ├── Table.js         # Table model
│   └── Reservation.js   # Reservation model
├── routes/
│   ├── auth.js          # Auth routes
│   ├── tables.js        # Table routes
│   └── reservations.js  # Reservation routes
├── .env                 # Environment variables
├── server.js            # Main application file
└── package.json

Environment Variables

.env
PORT=5000
MONGODB_URI=mongodb+srv://your_connection_string
JWT_SECRET=your_jwt_secret_key_here

Database Schema

Let's design the database models for our application.

User Model

JavaScript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true
    },
    email: {
        type: String,
        required: true,
        unique: true,
        lowercase: true,
        trim: true
    },
    password: {
        type: String,
        required: true,
        minlength: 6
    },
    phone: {
        type: String,
        required: true
    }
}, {
    timestamps: true
});

// Hash password before saving
userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();
    
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

// Method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
    return bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Table Model

JavaScript
// models/Table.js
const mongoose = require('mongoose');

const tableSchema = new mongoose.Schema({
    tableNumber: {
        type: Number,
        required: true,
        unique: true
    },
    capacity: {
        type: Number,
        required: true,
        min: 1
    },
    location: {
        type: String,
        enum: ['indoor', 'outdoor', 'window'],
        default: 'indoor'
    },
    isAvailable: {
        type: Boolean,
        default: true
    }
}, {
    timestamps: true
});

module.exports = mongoose.model('Table', tableSchema);

Reservation Model

JavaScript
// models/Reservation.js
const mongoose = require('mongoose');

const reservationSchema = new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    table: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Table',
        required: true
    },
    date: {
        type: Date,
        required: true
    },
    time: {
        type: String,
        required: true
    },
    partySize: {
        type: Number,
        required: true,
        min: 1,
        max: 20
    },
    status: {
        type: String,
        enum: ['confirmed', 'cancelled', 'completed'],
        default: 'confirmed'
    },
    specialRequests: {
        type: String,
        maxlength: 500
    }
}, {
    timestamps: true
});

// Index for efficient queries
reservationSchema.index({ user: 1, date: 1 });
reservationSchema.index({ table: 1, date: 1, time: 1 });

module.exports = mongoose.model('Reservation', reservationSchema);
Database Indexes

We added indexes to the Reservation model:

  • { user: 1, date: 1 } - Find user's reservations by date
  • { table: 1, date: 1, time: 1 } - Check table availability

Indexes make queries much faster, especially with lots of data!

Authentication Routes

Let's create the authentication routes for user registration and login.

JavaScript
// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const { check, validationResult } = require('express-validator');
const User = require('../models/User');

// Generate JWT token
const generateToken = (id) => {
    return jwt.sign({ id }, process.env.JWT_SECRET, {
        expiresIn: '30d'
    });
};

// @route   POST /api/auth/register
// @desc    Register a new user
// @access  Public
router.post('/register', [
    check('name', 'Name is required').not.isEmpty(),
    check('email', 'Please include a valid email').isEmail(),
    check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
    check('phone', 'Phone number is required').not.isEmpty()
], async (req, res) => {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    const { name, email, password, phone } = req.body;

    try {
        // Check if user exists
        let user = await User.findOne({ email });
        if (user) {
            return res.status(400).json({ message: 'User already exists' });
        }

        // Create new user
        user = new User({ name, email, password, phone });
        await user.save();

        // Return token
        const token = generateToken(user._id);
        res.status(201.json({
            _id: user._id,
            name: user.name,
            email: user.email,
            token
        });
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

// @route   POST /api/auth/login
// @desc    Authenticate user & get token
// @access  Public
router.post('/login', [
    check('email', 'Please include a valid email').isEmail(),
    check('password', 'Password is required').exists()
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    const { email, password } = req.body;

    try {
        // Check for user
        const user = await User.findOne({ email });
        if (!user) {
            return res.status(400).json({ message: 'Invalid credentials' });
        }

        // Check password
        const isMatch = await user.comparePassword(password);
        if (!isMatch) {
            return res.status(400).json({ message: 'Invalid credentials' });
        }

        // Return token
        const token = generateToken(user._id);
        res.json({
            _id: user._id,
            name: user.name,
            email: user.email,
            token
        });
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

module.exports = router;
JWT Authentication

JWT (JSON Web Tokens) are used for authentication:

  1. User logs in with email/password
  2. Server validates and generates a JWT token
  3. Client includes the token in subsequent requests
  4. Server verifies token to authenticate requests

The token contains the user ID, signed with a secret key.

Table Management Routes

Let's create routes for managing restaurant tables.

JavaScript
// routes/tables.js
const express = require('express');
const router = express.Router();
const Table = require('../models/Table');

// @route   GET /api/tables
// @desc    Get all tables
// @access  Public
router.get('/', async (req, res) => {
    try {
        const tables = await Table.find();
        res.json(tables);
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

// @route   GET /api/tables/:id
// @desc    Get table by ID
// @access  Public
router.get('/:id', async (req, res) => {
    try {
        const table = await Table.findById(req.params.id);
        
        if (!table) {
            return res.status(404).json({ message: 'Table not found' });
        }
        
        res.json(table);
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

// @route   POST /api/tables
// @desc    Create a new table
// @access  Private (would add auth middleware)
router.post('/', async (req, res) => {
    try {
        const { tableNumber, capacity, location } = req.body;
        
        // Check if table number already exists
        let table = await Table.findOne({ tableNumber });
        if (table) {
            return res.status(400).json({ message: 'Table number already exists' });
        }
        
        table = new Table({ tableNumber, capacity, location });
        await table.save();
        
        res.status(201.json(table);
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

module.exports = router;

Reservation Routes

Now let's create the most important routes - for making reservations.

JavaScript
// routes/reservations.js
const express = require('express');
const router = express.Router();
const { check, validationResult } = require('express-validator');
const Reservation = require('../models/Reservation');
const Table = require('../models/Table');

// Middleware to protect routes (simplified)
const auth = (req, res, next) => {
    const token = req.header('x-auth-token');
    if (!token) {
        return res.status(401).json({ message: 'No token, authorization denied' });
    }
    // In production, verify token properly
    req.user = { id: 'user_id_from_token' };
    next();
};

// @route   GET /api/reservations
// @desc    Get current user's reservations
// @access  Private
router.get('/', auth, async (req, res) => {
    try {
        const reservations = await Reservation.find({ user: req.user.id })
            .populate('table', 'tableNumber capacity location')
            .sort({ date: 1 });
        
        res.json(reservations);
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

// @route   POST /api/reservations
// @desc    Create a new reservation
// @access  Private
router.post('/', [
    auth,
    check('tableId', 'Table ID is required').not.isEmpty(),
    check('date', 'Date is required').not.isEmpty(),
    check('time', 'Time is required').not.isEmpty(),
    check('partySize', 'Party size is required').not.isEmpty()
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    const { tableId, date, time, partySize, specialRequests } = req.body;

    try {
        // Check if table exists
        const table = await Table.findById(tableId);
        if (!table) {
            return res.status(404).json({ message: 'Table not found' });
        }

        // Check party size matches table capacity
        if (partySize > table.capacity) {
            return res.status(400.json({ 
                message: `Table capacity is ${table.capacity} guests` 
            }));
        }

        // Check for conflicting reservation
        const existingReservation = await Reservation.findOne({
            table: tableId,
            date: new Date(date),
            time: time,
            status: { $ne: 'cancelled' }
        });

        if (existingReservation) {
            return res.status(400).json({ 
                message: 'Table is already reserved for this time' 
            });
        }

        // Create reservation
        const reservation = new Reservation({
            user: req.user.id,
            table: tableId,
            date: new Date(date),
            time,
            partySize,
            specialRequests
        });

        await reservation.save();
        
        res.status(201.json(reservation);
    } catch (err) {
        console.error(err.message);
        res.status(500).json(message: 'Server error');
    }
});

// @route   DELETE /api/reservations/:id
// @desc    Cancel a reservation
// @access  Private
router.delete('/:id', auth, async (req, res) => {
    try {
        let reservation = await Reservation.findById(req.params.id);
        
        if (!reservation) {
            return res.status(404).json({ message: 'Reservation not found' });
        }

        // Check ownership
        if (reservation.user.toString() !== req.user.id) {
            return res.status(401).json({ message: 'Not authorized' });
        }

        // Update status to cancelled
        reservation.status = 'cancelled';
        await reservation.save();
        
        res.json({ message: 'Reservation cancelled' });
    } catch (err) {
        console.error(err.message);
        res.status(500).json({ message: 'Server error' });
    }
});

module.exports = router;
Validation is Critical

Notice how we validate:

  • Table exists before creating reservation
  • Party size doesn't exceed table capacity
  • No conflicting reservations at same time
  • User can only cancel their own reservations

Without these checks, your API could have serious bugs!

Input Validation

Let's create a central server file that ties everything together.

JavaScript
// server.js
require('dotenv'.config());
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

// Import routes
const authRoutes = require('./routes/auth');
const tableRoutes = require('./routes/tables');
const reservationRoutes = require('./routes/reservations');

const app = express();

// Middleware
app.use(express.json());
app.use(cors());

// Mount routes
app.use('/api/auth', authRoutes);
app.use('/api/tables', tableRoutes);
app.use('/api/reservations', reservationRoutes);

// Health check route
app.get('/api/health', (req, res) => {
    res.json({ status: 'ok', message: 'API is running' });
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ message: 'Something went wrong!' });
});

// Database connection
const PORT = process.env.PORT || 5000;
const MONGODB_URI = process.env.MONGODB_URI;

mongoose.connect(MONGODB_URI)
    .then(() => {
        console.log('Connected to MongoDB');
        app.listen(PORT, () => {
            console.log(`Server running on port ${PORT}`);
        });
    })
    .catch(err => {
        console.error('MongoDB connection error:', err);
        process.exit(1);
    });
Express-Validator

We use express-validator for input validation:

  • check('field').not().isEmpty() - Field is required
  • check('email').isEmail() - Valid email format
  • check('password').isLength({ min: 6 }) - Minimum length

Always validate input on the server - never trust client-side validation alone!

Testing the API

Now let's test our API using Postman or similar.

Starting the Server

Bash
# Run the server
node server.js

# Should see:
# Connected to MongoDB
# Server running on port 5000

Testing Endpoints

HTTP
# Register a user
POST http://localhost:5000/api/auth/register
Content-Type: application/json

{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123",
    "phone": "555-1234"
}

# Response:
# {
#   "_id": "...",
#   "name": "John Doe",
#   "email": "john@example.com",
#   "token": "eyJhbGciOiJIUzI1..."
# }
HTTP
# Login (using token from register)
POST http://localhost:5000/api/auth/login
Content-Type: application/json

{
    "email": "john@example.com",
    "password": "password123"
}

# Get all tables
GET http://localhost:5000/api/tables

# Create a reservation (with auth token)
POST http://localhost:5000/api/reservations
x-auth-token: your_token_here
Content-Type: application/json

{
    "tableId": "table_id_here",
    "date": "2026-03-20",
    "time": "19:00",
    "partySize": "4"
}
Testing Checklist
  • ✓ User can register with valid data
  • ✓ User can login and receive token
  • ✓ Protected routes require valid token
  • ✓ Validation errors return 400 status
  • ✓ Can create and cancel reservations
  • ✓ Cannot reserve already-booked table

Deployment

When you're ready to deploy your API to production.

Backend Hosting

  • Render - Free tier, supports Node.js
  • Railway - Easy deployment, good free tier
  • Heroku - Industry standard
  • DigitalOcean - More control

Database Hosting

  • MongoDB Atlas - Free cloud database
  • Atlas Search - Full-text search
Production Checklist
  • Use environment variables for secrets
  • Enable HTTPS
  • Set up proper JWT verification
  • Add rate limiting
  • Set up logging

Summary

Congratulations! You've built a complete Restaurant Reservation API.

What You Built

  • REST API - Full CRUD operations
  • Authentication - JWT-based user system
  • Database Models - Users, Tables, Reservations
  • Input Validation - Using express-validator
  • Error Handling - Centralized error handling

Key Concepts Learned

  • REST API design principles
  • MongoDB with Mongoose
  • JWT authentication
  • Password hashing with bcrypt
  • API testing with Postman

Next Steps

Add these features to your API:

  • Admin roles and permissions
  • Email confirmations
  • Availability calendar endpoints
  • Waitlist functionality
  • Table combination for large parties

Continue Learning

Ready for more? Try these tutorials:

  • Build a Blog CMS - Full-stack web development
  • Build a Real-time Chat App - WebSockets
  • Build a URL Shortener - API design