Build a Restaurant Reservation API
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.
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
- 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
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
# 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
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
# 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
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
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
// 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
// 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
// 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);
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.
// 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 (JSON Web Tokens) are used for authentication:
- User logs in with email/password
- Server validates and generates a JWT token
- Client includes the token in subsequent requests
- 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.
// 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.
// 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;
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.
// 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);
});
We use express-validator for input validation:
check('field').not().isEmpty()- Field is requiredcheck('email').isEmail()- Valid email formatcheck('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
# Run the server
node server.js
# Should see:
# Connected to MongoDB
# Server running on port 5000
Testing Endpoints
# 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..."
# }
# 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"
}
- ✓ 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
- 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